diff --git a/.gitignore b/.gitignore index 3940f035..9236f587 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,4 @@ build nbactions.xml nb-configuration.xml nbproject/ - -.junie -.claude +bin/* diff --git a/README.md b/README.md index 3243bdad..07d91a93 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,11 @@ Sailing quality-of-life for charting, navigation, facilities, and more. ![Trimmable Sails](docs/trimmable-sails.png) +## Trawling +- Highlight Net Buttons: Automatically highlights fishing net depth adjustment buttons when they need to be changed to match the current shoal depth. + - Calibration: Shows "Calibrating Nets..." message until the plugin observes a complete shoal movement cycle to sync timing. +- Show Net Capacity: Displays the current fish count in your nets (max 250 for two nets, 125 for one net). + ## Crewmates - Mute Overhead Text: Mute crewmate overhead messages. - Modes: `None` (default), `Other boats`, `All`. diff --git a/build.gradle b/build.gradle index 8b03f179..380668ed 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { testAnnotationProcessor 'org.projectlombok:lombok:1.18.30' testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.12.4' testImplementation group: 'net.runelite', name:'client', version: runeLiteVersion testImplementation group: 'net.runelite', name:'jshell', version: runeLiteVersion } diff --git a/src/main/java/com/duckblade/osrs/sailing/SailingConfig.java b/src/main/java/com/duckblade/osrs/sailing/SailingConfig.java index 25968c9f..fb012f9d 100644 --- a/src/main/java/com/duckblade/osrs/sailing/SailingConfig.java +++ b/src/main/java/com/duckblade/osrs/sailing/SailingConfig.java @@ -33,6 +33,14 @@ public interface SailingConfig extends Config ) String SECTION_FACILITIES = "facilities"; + @ConfigSection( + name = "Trawling", + description = "Settings for fishing net trawling.", + position = 250, + closedByDefault = true + ) + String SECTION_TRAWLING = "trawling"; + @ConfigSection( name = "Crewmates", description = "Settings for your crewmates.", @@ -399,6 +407,140 @@ default Color highlightCrystalExtractorInactiveColour() return Color.YELLOW; } + @ConfigItem( + keyName = "trawlingHighlightShoals", + name = "Highlight Shoals", + description = "Highlight fish shoals with a 10x10 tile area.", + section = SECTION_TRAWLING, + position = 1 + ) + default boolean trawlingHighlightShoals() + { + return false; + } + + @ConfigItem( + keyName = "trawlingShoalHighlightColour", + name = "Shoal Highlight Colour", + description = "Colour to highlight fish shoals.", + section = SECTION_TRAWLING, + position = 2 + ) + @Alpha + default Color trawlingShoalHighlightColour() + { + return Color.CYAN; + } + + @ConfigItem( + keyName = "trawlingShowNetCapacity", + name = "Show Net Capacity", + description = "Display the current fish count in your nets.", + section = SECTION_TRAWLING, + position = 3 + ) + default boolean trawlingShowNetCapacity() + { + return true; + } + + @ConfigItem( + keyName = "trawlingShowFishCaught", + name = "Show Fish Caught", + description = "Display the number of each fish caught in the session.", + section = SECTION_TRAWLING, + position = 4 + ) + default boolean trawlingShowFishCaught() + { + return true; + } + + @ConfigItem( + keyName = "trawlingShowNetDepthTimer", + name = "Show Net Depth Timer", + description = "Display an overlay showing ticks until net depth change.", + section = SECTION_TRAWLING, + position = 5 + ) + default boolean trawlingShowNetDepthTimer() + { + return false; + } + + @ConfigItem( + keyName = "trawlingShowShoalPaths", + name = "Show Shoal Routes", + description = "Display the known routes for shoals.", + section = SECTION_TRAWLING, + position = 6 + ) + default boolean trawlingShowShoalPaths() + { + return true; + } + + @ConfigItem( + keyName = "trawlingShoalPathColour", + name = "Route Colour", + description = "Colour for displaying shoal routes.", + section = SECTION_TRAWLING, + position = 7 + ) + @Alpha + default Color trawlingShoalPathColour() + { + return Color.WHITE; + } + + @ConfigItem( + keyName = "trawlingShowShoalDirectionArrows", + name = "Show Direction Arrows", + description = "Display directional arrows along shoal routes to indicate movement direction.", + section = SECTION_TRAWLING, + position = 8 + ) + default boolean trawlingShowShoalDirectionArrows() + { + return true; + } + + @ConfigItem( + keyName = "highlightNetButtons", + name = "Highlight Net Buttons ", + description = "Highlight the net button to move to the correct shoal depth.", + section = SECTION_TRAWLING, + position = 9 + ) + default boolean highlightNetButtons() + { + return true; + } + + @ConfigItem( + keyName = "notifyDepthChange", + name = "Notify Shoal Depth Changed", + description = "Notify you when the shoal changes depth.", + section = SECTION_TRAWLING, + position = 9 + ) + default Notification notifyDepthChange() + { + return Notification.OFF; + } + + @ConfigItem( + keyName = "notifyShoalMove", + name = "Notify Shoal Move", + description = "Notify you when the shoal moves.", + section = SECTION_TRAWLING, + position = 10 + ) + default Notification notifyShoalMove() + { + return Notification.OFF; + } + enum CrewmateMuteMode { NONE, diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/FishCaughtTracker.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/FishCaughtTracker.java new file mode 100644 index 00000000..f1eb6a0d --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/FishCaughtTracker.java @@ -0,0 +1,170 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.features.util.BoatTracker; +import com.duckblade.osrs.sailing.features.util.SailingUtil; +import com.duckblade.osrs.sailing.model.Boat; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.GameStateChanged; +import net.runelite.client.eventbus.Subscribe; +import org.apache.commons.lang3.ArrayUtils; + +@Slf4j +@Singleton +public class FishCaughtTracker implements PluginLifecycleComponent { + public static final Pattern CATCH_FISH_REGEX = + Pattern.compile("^(.+?) catch(?:es)? (an?|two|three|four|five|six) (.+?)!$"); + private final Client client; + private final BoatTracker boatTracker; + + /** + * All the fish that was caught into the net since it was last emptied. + */ + private final EnumMap fishInNet = new EnumMap<>(Shoal.class); + + /** + * All the fish that was collected by emptying the nets. + */ + private final EnumMap fishCollected = new EnumMap<>(Shoal.class); + + /** + * Creates a new FishCaughtTracker with the specified dependencies. + * + * @param client the RuneLite client instance + * @param boatTracker tracker for boat information including net capacity + */ + @Inject + public FishCaughtTracker(Client client, BoatTracker boatTracker) { + this.client = client; + this.boatTracker = boatTracker; + } + + @Override + public void startUp() { + log.debug("FishCaughtTracker started"); + reset(); + } + + @Override + public void shutDown() { + log.debug("FishCaughtTracker shut down"); + reset(); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged e) { + GameState state = e.getGameState(); + if (state == GameState.HOPPING || state == GameState.LOGGING_IN) { + log.debug("{}; nets are forcibly emptied", state); + log.debug("lost fish: {}", fishInNet); + fishInNet.clear(); + } + } + + @Subscribe + public void onChatMessage(ChatMessage e) { + if (!SailingUtil.isSailing(client) || + (e.getType() != ChatMessageType.GAMEMESSAGE && e.getType() != ChatMessageType.SPAM)) { + return; + } + + String message = e.getMessage(); + if (message.equals("You empty the nets into the cargo hold.")) { + // TODO: handle trying to empty net when already empty (in case of desync) + log.debug("Nets manually emptied; collecting fish: {}", fishInNet); + + for (var entry : fishInNet.entrySet()) { + fishCollected.merge(entry.getKey(), entry.getValue(), Integer::sum); + } + + fishInNet.clear(); + return; + } + + Matcher matcher = CATCH_FISH_REGEX.matcher(message); + if (!matcher.find()) { + return; + } + + String catcher = matcher.group(1); + String quantityWord = matcher.group(2); + String fish = matcher.group(3); + + int quantity = wordToNumber(quantityWord); + if (quantity == -1) { + log.debug("Unable to find quantity for message {}", message); + return; + } + + final var shoal = Shoal.byName(fish); + if (shoal == null) { + return; + } + + log.debug(message); + log.debug("{} {} caught by {}; total: {}", quantity, fish, catcher, fishInNet.get(shoal)); + fishInNet.merge(shoal, quantity, Integer::sum); + } + + private int wordToNumber(String word) { + if (word.equals("an")) { + word = "a"; + } + + String[] words = {"a", "two", "three", "four", "five", "six"}; + int wordIndex = ArrayUtils.indexOf(words, word); + + if (wordIndex == ArrayUtils.INDEX_NOT_FOUND) { + log.debug("Unable to find quantity for word {}", word); + return -1; + } + + return wordIndex + 1; + } + + /** + * Gets the current net capacity based on the player's boat. + * + * @return the net capacity, or 0 if no boat is available + */ + public int getNetCapacity() { + Boat boat = boatTracker.getBoat(); + return boat != null ? boat.getNetCapacity() : 0; + } + + public int getFishInNetCount() { + return fishInNet.values() + .stream() + .reduce(Integer::sum) + .orElse(0); + } + + /** + * All fish caught, either currently in the net or previously collected. + */ + public Map getFishCaught() { + var fishCaught = new EnumMap<>(fishCollected); + for (var entry : fishInNet.entrySet()) { + fishCaught.merge(entry.getKey(), entry.getValue(), Integer::sum); + } + + return Collections.unmodifiableMap(fishCaught); + } + + private void reset() { + fishInNet.clear(); + fishCollected.clear(); + } +} diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/NetDepthButtonHighlighter.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/NetDepthButtonHighlighter.java new file mode 100644 index 00000000..88c62f8e --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/NetDepthButtonHighlighter.java @@ -0,0 +1,443 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.features.util.BoatTracker; +import com.duckblade.osrs.sailing.model.Boat; +import com.duckblade.osrs.sailing.model.ShoalDepth; +import com.duckblade.osrs.sailing.model.SizeClass; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import com.google.common.collect.Range; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.VarbitChanged; +import net.runelite.api.gameval.InterfaceID; +import net.runelite.api.gameval.VarbitID; +import net.runelite.api.widgets.Widget; +import net.runelite.client.eventbus.Subscribe; +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 javax.inject.Singleton; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +/** + * Overlay component that highlights net depth adjustment buttons when shoal depth is known. + * Highlights buttons to guide players toward matching their net depth to the current shoal depth. + */ +@Slf4j +@Singleton +public class NetDepthButtonHighlighter extends Overlay + implements PluginLifecycleComponent { + + // Widget indices for fishing net controls + private int starboardNetDownWidgetIndex; + private int starboardNetUpWidgetIndex; + private int starboardNetSpriteIndex; + private int portNetDownWidgetIndex; + private int portNetUpWidgetIndex; + private int portNetSpriteIndex; + private final ShoalTracker shoalTracker; + private final NetDepthTracker netDepthTracker; + private final BoatTracker boatTracker; + private final Client client; + private final SailingConfig config; + + // Cached highlighting state to avoid recalculating every frame + private boolean shouldHighlightPort = false; + private boolean shouldHighlightStarboard = false; + private ShoalDepth cachedRequiredDepth = null; + private ShoalDepth cachedPortDepth = null; + private ShoalDepth cachedStarboardDepth = null; + private boolean highlightingStateValid = false; + + /** + * Creates a new NetDepthButtonHighlighter with the specified dependencies. + * + * @param shoalTracker tracker for shoal state and depth + * @param netDepthTracker tracker for current net depths + * @param boatTracker tracker for boat information + * @param client the RuneLite client instance + * @param config sailing configuration settings + */ + @Inject + public NetDepthButtonHighlighter(ShoalTracker shoalTracker, + NetDepthTracker netDepthTracker, + BoatTracker boatTracker, + Client client, + SailingConfig config) { + this.shoalTracker = shoalTracker; + this.netDepthTracker = netDepthTracker; + this.boatTracker = boatTracker; + this.client = client; + this.config = config; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_WIDGETS); + setPriority(1000.0f); + } + + @Override + public boolean isEnabled(SailingConfig config) { + return config.highlightNetButtons(); + } + + @Override + public void startUp() { + log.debug("NetDepthButtonHighlighter started"); + invalidateHighlightingState(); + } + + @Override + public void shutDown() { + log.debug("NetDepthButtonHighlighter shut down"); + invalidateHighlightingState(); + } + + @Override + public Dimension render(Graphics2D graphics) { + if (!validatePrerequisites()) { + return null; + } + + ensureHighlightingStateValid(); + + if (!hasHighlightsToRender()) { + return null; + } + + Widget sailingWidget = getSailingWidget(); + if (sailingWidget == null) { + return null; + } + + renderCachedHighlights(graphics, sailingWidget); + return null; + } + + private void SetWidgetIndexForSloop() + { + Widget sailingInterface = getSailingWidget(); + Widget[] children = sailingInterface.getChildren(); + int NET_TO_DOWN_BUTTON_DELTA = 1; + int NET_TO_UP_BUTTON_DELTA = 12; + if (children == null) return; + int spriteID; + + boolean firstSpriteFound = false; + for (Widget child : children) { + spriteID = child.getSpriteId(); + if (!isANetSprite(spriteID)) continue; + if (!firstSpriteFound) + { + log.debug("first found at {}", child.getIndex()); + starboardNetSpriteIndex = child.getIndex(); + starboardNetDownWidgetIndex = child.getIndex() + NET_TO_DOWN_BUTTON_DELTA; + starboardNetUpWidgetIndex = child.getIndex() + NET_TO_UP_BUTTON_DELTA; + firstSpriteFound = true; + continue; + } + log.debug("second found at {}", child.getIndex()); + portNetSpriteIndex = child.getIndex(); + portNetDownWidgetIndex = child.getIndex() + NET_TO_DOWN_BUTTON_DELTA; + portNetUpWidgetIndex = child.getIndex() + NET_TO_UP_BUTTON_DELTA; + } + } + + private boolean isANetSprite(int spriteID) + { + int netMinSpriteID = 7080; + int netMaxSpiteID = 7083; + Range validRange = Range.closed(netMinSpriteID, netMaxSpiteID); + return validRange.contains(spriteID); + } + + private void SetWidgetIndexForSkiff() + { + Widget sailingInterface = getSailingWidget(); + Widget[] children = sailingInterface.getChildren(); + int NET_TO_DOWN_BUTTON_DELTA = 1; + int NET_TO_UP_BUTTON_DELTA = 12; + int NET_SPRITE_ID = 7080; + if (children == null) return; + for (Widget child : children) { + int spriteID = child.getSpriteId(); + if (spriteID != NET_SPRITE_ID) continue; + starboardNetSpriteIndex = child.getIndex(); + starboardNetDownWidgetIndex = child.getIndex() + NET_TO_DOWN_BUTTON_DELTA; + starboardNetUpWidgetIndex = child.getIndex() + NET_TO_UP_BUTTON_DELTA; + } + } + + private boolean validatePrerequisites() { + if (!canHighlightButtons()) { + if (highlightingStateValid) { + invalidateHighlightingState(); + } + return false; + } + return true; + } + + private void ensureHighlightingStateValid() { + if (!highlightingStateValid) { + updateHighlightingState(); + } + } + + private boolean hasHighlightsToRender() { + return shouldHighlightPort || shouldHighlightStarboard; + } + + private Widget getSailingWidget() { + return client.getWidget(InterfaceID.SailingSidepanel.FACILITIES_ROWS); + } + + private boolean canHighlightButtons() { + Boat boat = boatTracker.getBoat(); + if (boat == null || boat.getFishingNets().isEmpty()) { + return false; + } + + return shoalTracker.hasShoal() && shoalTracker.isShoalDepthKnown(); + } + + private void invalidateHighlightingState() { + highlightingStateValid = false; + shouldHighlightPort = false; + shouldHighlightStarboard = false; + cachedRequiredDepth = null; + cachedPortDepth = null; + cachedStarboardDepth = null; + } + + private void updateHighlightingState() { + netDepthTracker.refreshCache(); + cacheCurrentDepths(); + calculateHighlightingDecisions(); + highlightingStateValid = true; + } + + private void cacheCurrentDepths() { + cachedRequiredDepth = determineRequiredDepth(); + cachedPortDepth = netDepthTracker.getPortNetDepth(); + cachedStarboardDepth = netDepthTracker.getStarboardNetDepth(); + } + + private void calculateHighlightingDecisions() { + shouldHighlightPort = shouldHighlightNet(cachedPortDepth); + shouldHighlightStarboard = shouldHighlightNet(cachedStarboardDepth); + } + + private boolean shouldHighlightNet(ShoalDepth netDepth) { + return cachedRequiredDepth != null && + cachedRequiredDepth != ShoalDepth.UNKNOWN && + netDepth != null && + netDepth != cachedRequiredDepth; + } + + + + private void renderCachedHighlights(Graphics2D graphics, Widget parent) { + Color highlightColor = config.trawlingShoalHighlightColour(); + + if (shouldHighlightStarboard) { + renderNetHighlight(graphics, parent, highlightColor, + starboardNetSpriteIndex, starboardNetUpWidgetIndex, starboardNetDownWidgetIndex, cachedStarboardDepth); + } + + if (shouldHighlightPort) { + renderNetHighlight(graphics, parent, highlightColor, + portNetSpriteIndex, portNetUpWidgetIndex, portNetDownWidgetIndex, cachedPortDepth); + } + } + + private void renderNetHighlight(Graphics2D graphics, Widget parent, Color highlightColor, + int netSpriteIndex, int netUpWidgetIndex, int netDownWidgetIndex, ShoalDepth cachedDepth) { + Widget netDepthWidget = parent.getChild(netSpriteIndex); + if (netDepthWidget == null) return; + if (starboardNetUpWidgetIndex == 0 || starboardNetDownWidgetIndex == 0) + { + initializeWidgetIndices(); + } + + if (isWidgetInteractable(netDepthWidget)) { + highlightNetButton(graphics, parent, cachedDepth, cachedRequiredDepth, + netUpWidgetIndex, netDownWidgetIndex, highlightColor); + } + } + + private boolean isWidgetInteractable(Widget widget) { + return widget != null && widget.getOpacity() == 0; + } + + @Subscribe + public void onGameTick(GameTick e) { + if (!highlightingStateValid) { + return; + } + + if (hasShoalDepthChanged()) { + invalidateHighlightingState(); + return; + } + + if (haveNetDepthsChanged()) { + invalidateHighlightingState(); + } + } + + private boolean hasShoalDepthChanged() { + ShoalDepth currentRequiredDepth = determineRequiredDepth(); + return currentRequiredDepth != cachedRequiredDepth; + } + + private boolean haveNetDepthsChanged() { + ShoalDepth currentPortDepth = netDepthTracker.getPortNetDepth(); + ShoalDepth currentStarboardDepth = netDepthTracker.getStarboardNetDepth(); + + return currentPortDepth != cachedPortDepth || currentStarboardDepth != cachedStarboardDepth; + } + + @Subscribe + public void onVarbitChanged(VarbitChanged e) { + int varbitId = e.getVarbitId(); + if (isNetDepthVarbit(varbitId)) { + invalidateHighlightingState(); + } + } + + private boolean isNetDepthVarbit(int varbitId) { + return varbitId == VarbitID.SAILING_SIDEPANEL_BOAT_TRAWLING_NET_0_DEPTH || + varbitId == VarbitID.SAILING_SIDEPANEL_BOAT_TRAWLING_NET_1_DEPTH; + } + + private ShoalDepth determineRequiredDepth() { + if (!shoalTracker.isShoalDepthKnown()) { + return null; + } + return shoalTracker.getCurrentShoalDepth(); + } + + private void highlightNetButton(Graphics2D graphics, Widget parent, ShoalDepth current, + ShoalDepth required, int upIndex, int downIndex, Color color) { + int buttonIndex = getButtonIndex(current, required, upIndex, downIndex); + Widget button = getNetWidget(parent, buttonIndex); + + if (isButtonHighlightable(button, parent)) { + drawButtonHighlight(graphics, button, color); + } + } + + private int getButtonIndex(ShoalDepth current, ShoalDepth required, int upIndex, int downIndex) { + return required.ordinal() < current.ordinal() ? upIndex : downIndex; + } + + private boolean isButtonHighlightable(Widget button, Widget parent) { + return button != null && + !button.isHidden() && + hasValidBounds(button) && + isWidgetInViewport(button, parent); + } + + private boolean hasValidBounds(Widget button) { + Rectangle bounds = button.getBounds(); + return bounds.width > 0 && bounds.height > 0; + } + + private void drawButtonHighlight(Graphics2D graphics, Widget button, Color color) { + Rectangle bounds = button.getBounds(); + graphics.setColor(color); + graphics.setStroke(new BasicStroke(3)); + graphics.drawRect(bounds.x, bounds.y, bounds.width, bounds.height); + } + + + + private Widget getNetWidget(Widget parent, int index) { + Widget parentWidget = parent.getChild(index); + if (parentWidget == null) { + return null; + } + + Rectangle bounds = parentWidget.getBounds(); + if (bounds.x == -1 && bounds.y == -1) { + return findChildWithValidBounds(parentWidget); + } + + return parentWidget; + } + + private Widget findChildWithValidBounds(Widget parentWidget) { + Widget[] children = parentWidget.getChildren(); + if (children != null) { + for (Widget child : children) { + if (child != null && hasValidBounds(child)) { + return child; + } + } + } + return null; + } + + private boolean isWidgetInViewport(Widget widget, Widget scrollContainer) { + if (widget == null || scrollContainer == null) { + return false; + } + + Rectangle widgetBounds = widget.getBounds(); + Widget scrollViewport = findScrollViewport(scrollContainer); + + if (scrollViewport == null) { + Rectangle containerBounds = scrollContainer.getBounds(); + return containerBounds.contains(widgetBounds); + } + + Rectangle viewportBounds = scrollViewport.getBounds(); + Rectangle visibleArea = new Rectangle( + viewportBounds.x, + viewportBounds.y, + viewportBounds.width, + viewportBounds.height + ); + + return visibleArea.contains(widgetBounds); + } + + private Widget findScrollViewport(Widget scrollContainer) { + Widget scrollViewport = scrollContainer; + while (scrollViewport != null && scrollViewport.getScrollHeight() == 0) { + scrollViewport = scrollViewport.getParent(); + } + return scrollViewport; + } + + private void initializeWidgetIndices() + { + Boat boat = boatTracker.getBoat(); + if (boat == null) return; + + SizeClass boatSize = boat.getSizeClass(); + log.debug("Boat Size: {}", boatSize); + switch(boatSize) + { + case SLOOP: + SetWidgetIndexForSloop(); + break; + case SKIFF: + SetWidgetIndexForSkiff(); + break; + default: + return; + } + log.debug("Starboard Down Found at {}", starboardNetDownWidgetIndex); + log.debug("Starboard Up Found at {}", starboardNetUpWidgetIndex); + log.debug("Port Down Found at {}", portNetDownWidgetIndex); + log.debug("Port Up Found at {}", portNetUpWidgetIndex); + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/NetDepthTimer.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/NetDepthTimer.java new file mode 100644 index 00000000..8aad507e --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/NetDepthTimer.java @@ -0,0 +1,253 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.features.util.SailingUtil; +import com.duckblade.osrs.sailing.model.FishingAreaType; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameTick; +import net.runelite.client.eventbus.Subscribe; +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 javax.inject.Singleton; +import java.awt.Dimension; +import java.awt.Graphics2D; + +@Slf4j +@Singleton +public class NetDepthTimer extends Overlay implements PluginLifecycleComponent { + + // Number of ticks shoal must be moving before we consider it "was moving" + private static final int MOVEMENT_THRESHOLD_TICKS = 5; + + // Number of ticks at same position to consider shoal "stopped" + private static final int STOPPED_THRESHOLD_TICKS = 2; + + private final Client client; + private final ShoalTracker shoalTracker; + + // Movement tracking + private WorldPoint lastShoalPosition = null; + private int ticksAtSamePosition = 0; + private int ticksMoving = 0; + private boolean hasBeenMoving = false; + + // Timer state + private int timerTicks = 0; + private boolean timerActive = false; + + /** + * Creates a new NetDepthTimer with the specified dependencies. + * + * @param client the RuneLite client instance + * @param shoalTracker tracker for shoal state and movement + */ + @Inject + public NetDepthTimer(Client client, ShoalTracker shoalTracker) { + this.client = client; + this.shoalTracker = shoalTracker; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_WIDGETS); + setPriority(1000.0f); + } + + @Override + public boolean isEnabled(SailingConfig config) { + return config.trawlingShowNetDepthTimer(); + } + + @Override + public void startUp() { + log.debug("NetDepthTimer started"); + } + + @Override + public void shutDown() { + log.debug("NetDepthTimer shut down"); + resetState(); + } + + /** + * Gets current timer information for display in overlay. + * + * @return timer information, or null if no shoal or timer is disabled + */ + public TimerInfo getTimerInfo() { + if (!shoalTracker.hasShoal()) { + return null; + } + + // Disable timer in ONE_DEPTH areas (Giant Krill areas) + WorldPoint playerLocation = SailingUtil.getTopLevelWorldPoint(client); + FishingAreaType areaType = TrawlingData.FishingAreas.getFishingAreaType(playerLocation); + if (areaType == FishingAreaType.ONE_DEPTH) { + return null; // Timer disabled in krill areas + } + + boolean shoalIsMoving = ticksAtSamePosition < STOPPED_THRESHOLD_TICKS; + + if (!timerActive) { + if (shoalIsMoving) { + return new TimerInfo(false, true, 0, false); // Waiting for shoal to stop + } else { + return new TimerInfo(false, false, 0, false); // Calibrating + } + } + + // Timer counts through full duration + int shoalDuration = shoalTracker.getShoalDuration(); + int depthsPerStop = 2; + int depthChangeTime = shoalDuration / depthsPerStop; + + if (timerTicks < depthChangeTime) { + // First phase: countdown to depth change + int ticksUntilDepthChange = depthChangeTime - timerTicks; + return new TimerInfo(true, false, Math.max(0, ticksUntilDepthChange), false); + } else { + // Second phase: countdown to movement + int ticksUntilMovement = shoalDuration - timerTicks; + return new TimerInfo(true, false, Math.max(0, ticksUntilMovement), true); + } + } + + + + @Subscribe + public void onGameTick(GameTick e) { + if (!shoalTracker.hasShoal()) { + // No shoal - reset state + if (timerActive || hasBeenMoving) { + log.debug("No shoal detected - resetting timer state"); + resetState(); + } + return; + } + + // Disable timer processing in ONE_DEPTH areas (Giant Krill areas) + WorldPoint playerLocation = SailingUtil.getTopLevelWorldPoint(client); + FishingAreaType areaType = TrawlingData.FishingAreas.getFishingAreaType(playerLocation); + if (areaType == FishingAreaType.ONE_DEPTH) { + // Reset timer state if we're in a krill area + if (timerActive || hasBeenMoving) { + resetState(); + } + return; + } + + // Check if WorldEntity is valid, try to find it if not + if (shoalTracker.isShoalEntityInvalid()) { + shoalTracker.findShoalEntity(); + if (shoalTracker.isShoalEntityInvalid()) { + // WorldEntity is truly gone - reset state + log.debug("WorldEntity no longer exists - resetting timer state"); + resetState(); + return; + } + } + + // Update location in tracker and track movement + shoalTracker.updateLocation(); + WorldPoint currentPos = shoalTracker.getCurrentLocation(); + if (currentPos != null) { + trackMovement(currentPos); + } + + // Update timer if active + if (timerActive) { + timerTicks++; + int shoalDuration = shoalTracker.getShoalDuration(); + if (timerTicks >= shoalDuration) { + // Full duration reached - stop timer (shoal should start moving) + timerActive = false; + log.debug("Full duration reached at {} ticks (shoal should move)", timerTicks); + } + } + } + + private void trackMovement(WorldPoint currentPos) { + if (currentPos.equals(lastShoalPosition)) { + // Shoal is stationary + ticksAtSamePosition++; + ticksMoving = 0; + + // Check if shoal just stopped after being in motion + if (ticksAtSamePosition == STOPPED_THRESHOLD_TICKS && hasBeenMoving) { + startTimer(); + } + } else { + // Shoal is moving + lastShoalPosition = currentPos; + ticksAtSamePosition = 0; + ticksMoving++; + + // Mark as having been moving if it's moved for enough ticks + if (ticksMoving >= MOVEMENT_THRESHOLD_TICKS) { + hasBeenMoving = true; + } + + // Stop timer if shoal starts moving again + if (timerActive) { + timerActive = false; + log.debug("Timer stopped - shoal started moving"); + } + } + } + + private void startTimer() { + int shoalDuration = shoalTracker.getShoalDuration(); + if (shoalDuration > 0) { + timerActive = true; + timerTicks = 0; + log.debug("Timer started - shoal stopped after movement (duration: {} ticks)", shoalDuration); + } + } + + private void resetState() { + lastShoalPosition = null; + ticksAtSamePosition = 0; + ticksMoving = 0; + hasBeenMoving = false; + timerActive = false; + timerTicks = 0; + } + + @Override + public Dimension render(Graphics2D graphics) { + // Timer display is handled by TrawlingOverlay + return null; + } + + /** + * Data class for exposing timer information to overlay + */ + public static class TimerInfo { + @Getter + private final boolean active; + @Getter + private final boolean waiting; + private final int ticksRemaining; + @Getter + private final boolean postDepthChange; + + public TimerInfo(boolean active, boolean waiting, int ticksRemaining, boolean postDepthChange) { + this.active = active; + this.waiting = waiting; + this.ticksRemaining = ticksRemaining; + this.postDepthChange = postDepthChange; + } + + public int getTicksUntilDepthChange() { + return ticksRemaining; + } + + public int getTicksUntilMovement() { + return ticksRemaining; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/NetDepthTracker.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/NetDepthTracker.java new file mode 100644 index 00000000..4f2a8414 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/NetDepthTracker.java @@ -0,0 +1,164 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.model.ShoalDepth; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.events.VarbitChanged; +import net.runelite.api.gameval.VarbitID; +import net.runelite.client.eventbus.Subscribe; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Service component that tracks the current depth of both trawling nets using varbits + */ +@Slf4j +@Singleton +public class NetDepthTracker implements PluginLifecycleComponent { + + // Varbit IDs for trawling net depths + private static final int TRAWLING_NET_PORT_VARBIT = VarbitID.SAILING_SIDEPANEL_BOAT_TRAWLING_NET_1_DEPTH; + private static final int TRAWLING_NET_STARBOARD_VARBIT = VarbitID.SAILING_SIDEPANEL_BOAT_TRAWLING_NET_0_DEPTH; + + private final Client client; + + // Cached values for performance + private ShoalDepth portNetDepth; + private ShoalDepth starboardNetDepth; + private boolean portCacheValid = false; + private boolean starboardCacheValid = false; + + /** + * Creates a new NetDepthTracker with the specified client. + * + * @param client the RuneLite client instance + */ + @Inject + public NetDepthTracker(Client client) { + this.client = client; + } + + @Override + public void startUp() { + log.debug("NetDepthTracker started"); + // Don't read varbits during startup - they will be read lazily when needed + } + + @Override + public void shutDown() { + log.debug("NetDepthTracker shut down"); + invalidateCache(); + } + + /** + * Gets the current port net depth. + * + * @return the port net depth, or null if net is not lowered + */ + public ShoalDepth getPortNetDepth() { + if (!portCacheValid) { + portNetDepth = getNetDepthFromVarbit(TRAWLING_NET_PORT_VARBIT); + portCacheValid = true; + } + return portNetDepth; + } + + /** + * Gets the current starboard net depth. + * + * @return the starboard net depth, or null if net is not lowered + */ + public ShoalDepth getStarboardNetDepth() { + if (!starboardCacheValid) { + starboardNetDepth = getNetDepthFromVarbit(TRAWLING_NET_STARBOARD_VARBIT); + starboardCacheValid = true; + } + return starboardNetDepth; + } + + /** + * Checks if both nets are at the same depth. + * + * @return true if both nets are at the same depth, false otherwise + */ + public boolean areNetsAtSameDepth() { + ShoalDepth port = getPortNetDepth(); + ShoalDepth starboard = getStarboardNetDepth(); + return port != null && port == starboard; + } + + /** + * Checks if both nets are at the specified depth. + * + * @param targetDepth the depth to check against + * @return true if both nets are at the target depth, false otherwise + */ + public boolean areNetsAtDepth(ShoalDepth targetDepth) { + return getPortNetDepth() == targetDepth && getStarboardNetDepth() == targetDepth; + } + + @Subscribe + public void onVarbitChanged(VarbitChanged e) { + int varbitId = e.getVarbitId(); + + if (varbitId == TRAWLING_NET_PORT_VARBIT) { + portNetDepth = getNetDepthFromVarbit(TRAWLING_NET_PORT_VARBIT); + portCacheValid = true; + } else if (varbitId == TRAWLING_NET_STARBOARD_VARBIT) { + starboardNetDepth = getNetDepthFromVarbit(TRAWLING_NET_STARBOARD_VARBIT); + starboardCacheValid = true; + } + } + + /** + * Convert varbit value to ShoalDepth enum + */ + private ShoalDepth getNetDepthFromVarbit(int varbitId) { + int varbitValue = client.getVarbitValue(varbitId); + + // Convert varbit value to ShoalDepth (0=net not lowered, 1=shallow, 2=moderate, 3=deep) + switch (varbitValue) { + case 0: + return null; // Net not lowered + case 1: + return ShoalDepth.SHALLOW; + case 2: + return ShoalDepth.MODERATE; + case 3: + return ShoalDepth.DEEP; + default: + log.warn("Unknown varbit value for net depth: {} (varbit: {})", varbitValue, varbitId); + return null; + } + } + + /** + * Update cached values from current varbit state + */ + private void updateCachedValues() { + portNetDepth = getNetDepthFromVarbit(TRAWLING_NET_PORT_VARBIT); + starboardNetDepth = getNetDepthFromVarbit(TRAWLING_NET_STARBOARD_VARBIT); + portCacheValid = true; + starboardCacheValid = true; + } + + /** + * Forces refresh of cached values (useful for debugging or when cache might be stale). + */ + public void refreshCache() { + invalidateCache(); + updateCachedValues(); + } + + /** + * Invalidates the cache, forcing fresh reads on next access. + */ + private void invalidateCache() { + portNetDepth = null; + starboardNetDepth = null; + portCacheValid = false; + starboardCacheValid = false; + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/PathSmoothingDemo.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/PathSmoothingDemo.java new file mode 100644 index 00000000..f2caf4c2 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/PathSmoothingDemo.java @@ -0,0 +1,41 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.features.trawling.ShoalPathData.*; + +/** + * Demo class to test path smoothing on existing routes. + * Run this to see the potential improvements from applying path smoothing + * to the 6 existing routes in the ShoalPathData folder. + */ +public class PathSmoothingDemo { + + public static void main(String[] args) { + System.out.println("=== Path Smoothing Analysis for Existing Routes ===\n"); + + // Analyze all existing routes + analyzeRoute("HalibutPortRoberts", HalibutPortRoberts.INSTANCE.getWaypoints()); + analyzeRoute("HalibutSouthernExpanse", HalibutSouthernExpanse.INSTANCE.getWaypoints()); + analyzeRoute("BluefinBuccaneersHaven", BluefinBuccaneersHaven.INSTANCE.getWaypoints()); + analyzeRoute("BluefinRainbowReef", BluefinRainbowReef.INSTANCE.getWaypoints()); + analyzeRoute("MarlinWeissmere", MarlinWeissmere.INSTANCE.getWaypoints()); + analyzeRoute("MarlinBrittleIsle", MarlinBrittleIsle.INSTANCE.getWaypoints()); + + System.out.println("=== Analysis Complete ==="); + System.out.println("To apply smoothing, copy the generated code from the output above"); + System.out.println("and replace the WAYPOINTS arrays in the respective route files."); + } + + private static void analyzeRoute(String routeName, ShoalWaypoint[] waypoints) { + System.out.println("--- " + routeName + " ---"); + + // Print analysis + String analysis = PathSmoothingUtil.analyzePath(waypoints); + System.out.println(analysis); + + // Generate smoothed code + String smoothedCode = PathSmoothingUtil.generateSmoothedCode(waypoints, routeName); + System.out.println("Smoothed code for " + routeName + ":\n" + smoothedCode); + + System.out.println(""); // Empty line for readability + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/PathSmoothingUtil.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/PathSmoothingUtil.java new file mode 100644 index 00000000..b436f13a --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/PathSmoothingUtil.java @@ -0,0 +1,278 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.google.common.math.DoubleMath; +import net.runelite.api.coords.WorldPoint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for applying path smoothing algorithms to existing shoal routes. + * This can be used to clean up pre-generated paths by removing unnecessary zigzags + * and waypoints that don't meaningfully contribute to navigation. + */ +public class PathSmoothingUtil { + + private static final int MAX_WAYPOINT_DISTANCE = 30; // World coordinate units (tiles) + + /** + * Applies path smoothing to an array of ShoalWaypoints, preserving stop points. + * + * @param originalWaypoints the original waypoints to smooth + * @return smoothed waypoints with unnecessary points removed + */ + public static ShoalWaypoint[] smoothPath(ShoalWaypoint[] originalWaypoints) { + if (originalWaypoints == null || originalWaypoints.length < 3) { + return originalWaypoints; // Can't smooth paths with less than 3 points + } + + List smoothedPath = new ArrayList<>(); + + // Always keep the first waypoint + smoothedPath.add(originalWaypoints[0]); + + for (int i = 1; i < originalWaypoints.length - 1; i++) { + ShoalWaypoint current = originalWaypoints[i]; + ShoalWaypoint previous = smoothedPath.get(smoothedPath.size() - 1); + ShoalWaypoint next = originalWaypoints[i + 1]; + + // Always keep stop points + if (current.isStopPoint()) { + smoothedPath.add(current); + continue; + } + + // Check if this waypoint should be kept or smoothed out + if (shouldKeepWaypoint(previous.getPosition(), current.getPosition(), next.getPosition())) { + smoothedPath.add(current); + } + // If not kept, the waypoint is effectively removed (smoothed out) + } + + // Always keep the last waypoint + smoothedPath.add(originalWaypoints[originalWaypoints.length - 1]); + + return smoothedPath.toArray(new ShoalWaypoint[0]); + } + + /** + * Determines if a waypoint should be kept based on path smoothing criteria. + * + * @param p1 previous waypoint position + * @param p2 current waypoint position + * @param p3 next waypoint position + * @return true if the waypoint should be kept, false if it should be smoothed out + */ + private static boolean shouldKeepWaypoint(WorldPoint p1, WorldPoint p2, WorldPoint p3) { + // Keep waypoint if segment is too long (might be important) + boolean isSegmentTooLong = !isNearPosition(p2, p1, MAX_WAYPOINT_DISTANCE); + if (isSegmentTooLong) { + return true; + } + + // Remove waypoint if the three points are nearly collinear (small zigzag) + if (arePointsNearlyCollinear(p1, p2, p3)) { + return false; + } + + // Remove waypoint if the deviation from direct path is small + if (isSmallDeviation(p1, p2, p3)) { + return false; + } + + // Remove waypoint if slopes are similar (more conservative than exact match) + double previousSlope = getSlope(p1, p2); + double currentSlope = getSlope(p2, p3); + if (DoubleMath.fuzzyEquals(previousSlope, currentSlope, 0.05)) { // More conservative: 0.05 instead of 0.1 + return false; + } + + // Keep the waypoint if none of the smoothing criteria apply + return true; + } + + /** + * Checks if two points are within a certain distance of each other. + */ + private static boolean isNearPosition(WorldPoint p1, WorldPoint p2, int range) { + int dx = p1.getX() - p2.getX(); + int dy = p1.getY() - p2.getY(); + int distanceSquared = dx * dx + dy * dy; + return distanceSquared < (range * range); + } + + /** + * Calculates the slope between two points. + */ + private static double getSlope(WorldPoint p1, WorldPoint p2) { + double dx = p1.getX() - p2.getX(); + double dy = p1.getY() - p2.getY(); + if (dy == 0) { + return dx == 0 ? 0 : Double.POSITIVE_INFINITY; + } + return dx / dy; + } + + /** + * Checks if three points are nearly collinear using the cross product method. + * Small cross products indicate the points are nearly in a straight line. + */ + private static boolean arePointsNearlyCollinear(WorldPoint p1, WorldPoint p2, WorldPoint p3) { + // Calculate cross product of vectors (p1->p2) and (p2->p3) + double dx1 = p2.getX() - p1.getX(); + double dy1 = p2.getY() - p1.getY(); + double dx2 = p3.getX() - p2.getX(); + double dy2 = p3.getY() - p2.getY(); + + double crossProduct = Math.abs(dx1 * dy2 - dy1 * dx2); + + // More conservative threshold - only remove very straight lines + // Reduced from 2.0 to 1.0 for more conservative smoothing + return crossProduct < 1.0; + } + + /** + * Checks if the middle point deviates only slightly from the direct path + * between the first and third points. Small deviations indicate unnecessary waypoints. + */ + private static boolean isSmallDeviation(WorldPoint p1, WorldPoint p2, WorldPoint p3) { + // Calculate distance from p2 to the line segment p1-p3 + double distanceToLine = distanceFromPointToLine(p2, p1, p3); + + // More conservative threshold - only remove points very close to the line + // Reduced from 3.0 to 1.5 tiles for more conservative smoothing + return distanceToLine < 1.5; + } + + /** + * Calculates the perpendicular distance from a point to a line segment. + */ + private static double distanceFromPointToLine(WorldPoint point, WorldPoint lineStart, WorldPoint lineEnd) { + double dx = lineEnd.getX() - lineStart.getX(); + double dy = lineEnd.getY() - lineStart.getY(); + + // If line segment has zero length, return distance to start point + if (dx == 0 && dy == 0) { + return Math.hypot(point.getX() - lineStart.getX(), point.getY() - lineStart.getY()); + } + + // Calculate the perpendicular distance using the formula: + // distance = |ax + by + c| / sqrt(a² + b²) + // where the line is ax + by + c = 0 + double a = dy; + double b = -dx; + double c = dx * lineStart.getY() - dy * lineStart.getX(); + + return Math.abs(a * point.getX() + b * point.getY() + c) / Math.sqrt(a * a + b * b); + } + + /** + * Analyzes a path and provides statistics about potential smoothing improvements. + * + * @param waypoints the waypoints to analyze + * @return analysis results as a formatted string + */ + public static String analyzePath(ShoalWaypoint[] waypoints) { + if (waypoints == null || waypoints.length < 3) { + return "Path too short to analyze (need at least 3 waypoints)"; + } + + ShoalWaypoint[] smoothed = smoothPath(waypoints); + int originalCount = waypoints.length; + int smoothedCount = smoothed.length; + int removedCount = originalCount - smoothedCount; + double reductionPercent = (removedCount / (double) originalCount) * 100; + + StringBuilder analysis = new StringBuilder(); + analysis.append(String.format("Path Analysis:\n")); + analysis.append(String.format(" Original waypoints: %d\n", originalCount)); + analysis.append(String.format(" Smoothed waypoints: %d\n", smoothedCount)); + analysis.append(String.format(" Removed waypoints: %d (%.1f%% reduction)\n", removedCount, reductionPercent)); + + // Count stop points + long stopPoints = java.util.Arrays.stream(waypoints).mapToLong(wp -> wp.isStopPoint() ? 1 : 0).sum(); + analysis.append(String.format(" Stop points preserved: %d\n", stopPoints)); + + // Note: Proximity validation temporarily disabled due to algorithm issues + analysis.append(" Proximity validation: DISABLED (conservative smoothing should be safe)\n"); + + return analysis.toString(); + } + + /** + * Validates that the smoothed path stays within a specified distance of the original path. + * This ensures that following the smoothed path will keep the boat close to the shoal. + * + * @param original the original waypoints + * @param smoothed the smoothed waypoints + * @param maxDistance maximum allowed distance in tiles + * @return validation result with deviation statistics + */ + private static PathProximityResult validatePathProximity(ShoalWaypoint[] original, ShoalWaypoint[] smoothed, double maxDistance) { + double maxDeviation = 0; + int worstSegmentIndex = -1; + + // Simple approach: for each smoothed segment, check the maximum distance + // from any original point to that segment line + for (int smoothedIndex = 0; smoothedIndex < smoothed.length - 1; smoothedIndex++) { + WorldPoint smoothedStart = smoothed[smoothedIndex].getPosition(); + WorldPoint smoothedEnd = smoothed[smoothedIndex + 1].getPosition(); + + // Check all original points against this smoothed segment + for (int originalIndex = 0; originalIndex < original.length; originalIndex++) { + WorldPoint originalPoint = original[originalIndex].getPosition(); + double deviation = distanceFromPointToLine(originalPoint, smoothedStart, smoothedEnd); + + if (deviation > maxDeviation) { + maxDeviation = deviation; + worstSegmentIndex = smoothedIndex; + } + } + } + + return new PathProximityResult(maxDeviation, maxDeviation <= maxDistance, worstSegmentIndex); + } + + /** + * Result of path proximity validation. + */ + private static class PathProximityResult { + final double maxDeviation; + final boolean staysWithinRange; + final int worstSegmentIndex; + + PathProximityResult(double maxDeviation, boolean staysWithinRange, int worstSegmentIndex) { + this.maxDeviation = maxDeviation; + this.staysWithinRange = staysWithinRange; + this.worstSegmentIndex = worstSegmentIndex; + } + } + + /** + * Generates the smoothed waypoint array as Java code that can be copied into the route files. + * + * @param waypoints the waypoints to smooth and format + * @param className the class name for the output + * @return formatted Java code string + */ + public static String generateSmoothedCode(ShoalWaypoint[] waypoints, String className) { + ShoalWaypoint[] smoothed = smoothPath(waypoints); + + StringBuilder code = new StringBuilder(); + code.append(String.format("// Smoothed waypoints for %s\n", className)); + code.append(String.format("// Original: %d waypoints, Smoothed: %d waypoints (%.1f%% reduction)\n", + waypoints.length, smoothed.length, + ((waypoints.length - smoothed.length) / (double) waypoints.length) * 100)); + code.append("public static final ShoalWaypoint[] WAYPOINTS = {\n"); + + for (ShoalWaypoint wp : smoothed) { + WorldPoint pos = wp.getPosition(); + code.append(String.format("\t\tnew ShoalWaypoint(new WorldPoint(%d, %d, %d), %s),\n", + pos.getX(), pos.getY(), pos.getPlane(), + wp.isStopPoint() ? "true" : "false")); + } + + code.append("\t};\n"); + return code.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/Shoal.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/Shoal.java new file mode 100644 index 00000000..04ed2a45 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/Shoal.java @@ -0,0 +1,48 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.model.FishingAreaType; +import java.awt.Color; +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Shoal +{ + + // Shoal durations here are 2 ticks lower than wiki numbers to handle movement tracking + GIANT_KRILL("Giant krill", "Giant blue krill", new Color(0xd7774d), FishingAreaType.ONE_DEPTH, 0), + HADDOCK("Haddock", "Golden haddock", new Color(0x7e919f), FishingAreaType.ONE_DEPTH, 0), + YELLOWFIN("Yellowfin", "Orangefin", new Color(0xebcd1c), FishingAreaType.TWO_DEPTH, 98), + HALIBUT("Halibut", "Huge halibut", new Color(0xb08f54), FishingAreaType.TWO_DEPTH, 78), + BLUEFIN("Bluefin", "Purplefin", new Color(0x2a89a8), FishingAreaType.THREE_DEPTH, 68), + MARLIN("Marlin", "Swift marlin", new Color(0xb9b7ad), FishingAreaType.THREE_DEPTH, 48); + + private static final Shoal[] VALUES = values(); + + private final String name; + private final String exoticName; + private final Color color; + private final FishingAreaType depth; + private final int stopDuration; + + @Override + public String toString() + { + return name; + } + + public static @Nullable Shoal byName(final String name) + { + for (final var shoal : VALUES) + { + if (shoal.name.equalsIgnoreCase(name) || shoal.exoticName.equalsIgnoreCase(name)) + { + return shoal; + } + } + + return null; + } +} diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalAreaData.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalAreaData.java new file mode 100644 index 00000000..7b4252e6 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalAreaData.java @@ -0,0 +1,75 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; + +/** + * Interface for shoal area data classes. + * Provides common methods for all shoal areas while allowing static usage. + */ +public interface ShoalAreaData { + + /** + * Get the area bounds for this shoal region. + */ + WorldArea getArea(); + + /** + * Get the complete waypoint path with stop point information. + */ + ShoalWaypoint[] getWaypoints(); + + /** + * Get the shoal type for this area. + */ + Shoal getShoalType(); + + /** + * Get the duration in ticks that shoals stop at each stop point in this area. + */ + default int getStopDuration() { return getShoalType().getStopDuration(); } + + // Default implementations for common operations + + /** + * Check if a world point is within this shoal area. + */ + default boolean contains(WorldPoint point) { + return getArea().contains(point); + } + + /** + * Get all waypoint positions as WorldPoint array (for compatibility). + */ + default WorldPoint[] getPositions() { + return ShoalWaypoint.getPositions(getWaypoints()); + } + + /** + * Get stop point indices (for compatibility). + */ + default int[] getStopIndices() { + return ShoalWaypoint.getStopIndices(getWaypoints()); + } + + /** + * Get the number of stop points in this area. + */ + default int getStopPointCount() { + return ShoalWaypoint.getStopPointCount(getWaypoints()); + } + + /** + * Get all stop point waypoints from the array. + */ + default ShoalWaypoint[] getStopPoints() { + return ShoalWaypoint.getStopPoints(getWaypoints()); + } + + /** + * Check if this area has valid data. + */ + default boolean isValidArea() { + return getArea() != null && getWaypoints() != null && getWaypoints().length > 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalFishingArea.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalFishingArea.java new file mode 100644 index 00000000..9931e26d --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalFishingArea.java @@ -0,0 +1,115 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.features.trawling.ShoalPathData.*; +import lombok.Getter; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; + +@Getter +public enum ShoalFishingArea +{ + GREAT_SOUND( + new WorldArea(1546, 3327, 93, 75, 0), + ShoalPaths.GIANT_KRILL_GREAT_SOUND, + new int[]{0, 18, 25, 31, 38, 50, 59}, + Shoal.GIANT_KRILL + ), + SIMIAN_SEA( + new WorldArea(2755, 2548, 103, 92, 0), + ShoalPaths.GIANT_KRILL_SIMIAN_SEA, + new int[]{0, 6, 12, 20, 27, 39, 49}, + Shoal.GIANT_KRILL + ), + SUNSET_BAY( + new WorldArea(1477, 2860, 128, 100, 0), + ShoalPaths.GIANT_KRILL_SUNSET_BAY, + new int[]{0, 9, 19, 37, 46, 52, 68}, + Shoal.GIANT_KRILL + ), + TURTLE_BELT( + new WorldArea(2922, 2465, 106, 112, 0), + ShoalPaths.GIANT_KRILL_TURTLE_BELT, + new int[]{0, 6, 20, 27, 33, 56, 66, 77}, + Shoal.GIANT_KRILL + ), + + ANGLERFISHS_LIGHT( + new WorldArea(2672, 2295, 162, 159, 0), + ShoalPaths.HADDOCK_ANGLERFISHS_LIGHT, + new int[]{0, 6, 22, 41, 48, 60, 74, 84}, + Shoal.HADDOCK + ), + MISTY_SEA( + new WorldArea(1377, 2607, 233, 182, 0), + ShoalPaths.HADDOCK_MISTY_SEA, + new int[]{0, 20, 45, 60, 64, 70, 87, 99, 112, 118}, + Shoal.HADDOCK + ), + THE_ONYX_CREST( + new WorldArea(2929, 2157, 196, 219, 0), + ShoalPaths.HADDOCK_THE_ONYX_CREST, + new int[]{0, 4, 15, 34, 52, 68, 83, 108, 129, 142}, + Shoal.HADDOCK + ), + + DEEPFIN_POINT( + new WorldArea(1781, 2665, 244, 216, 0), + ShoalPaths.YELLOWFIN_DEEPFIN_POINT, + new int[]{0, 20, 42, 74, 100, 117, 136, 163, 197, 211, 237}, + Shoal.YELLOWFIN + ), + SEA_OF_SOULS( + new WorldArea(2173, 2585, 192, 179, 0), + ShoalPaths.YELLOWFIN_SEA_OF_SOULS, + new int[]{0, 18, 38, 43, 53, 84, 107, 124, 140, 145, 155, 183}, + Shoal.YELLOWFIN + ), + THE_CROWN_JEWEL_TEMP( + new WorldArea(1633, 2533, 187, 199, 0), + ShoalPaths.YELLOWFIN_THE_CROWN_JEWEL, + new int[]{0, 23, 60, 80, 100, 109, 128, 154, 193}, + Shoal.YELLOWFIN + ), + // + PORT_ROBERTS(HalibutPortRoberts.INSTANCE), + SOUTHERN_EXPANSE(HalibutSouthernExpanse.INSTANCE), + BUCCANEERS_HAVEN(BluefinBuccaneersHaven.INSTANCE), + RAINBOW_REEF(BluefinRainbowReef.INSTANCE), + WEISSMERE(MarlinWeissmere.INSTANCE), + BRITTLE_ISLE(MarlinBrittleIsle.INSTANCE); + + static final ShoalFishingArea[] AREAS = values(); + + private final WorldArea area; + private final WorldPoint[] path; + private final int[] stopIndices; + private final Shoal shoal; + /** + * -- GETTER -- + * Get the ShoalAreaData interface if this area uses the new interface-based approach. + */ + private final ShoalAreaData areaData; // For interface-based entries + + // Constructor for legacy entries (4 parameters) + ShoalFishingArea(WorldArea area, WorldPoint[] path, int[] stopIndices, Shoal shoal) { + this.area = area; + this.path = path; + this.stopIndices = stopIndices; + this.shoal = shoal; + this.areaData = null; + } + + // Constructor for interface-based entries (single parameter) + ShoalFishingArea(ShoalAreaData areaData) { + this.areaData = areaData; + this.area = areaData.getArea(); + this.path = areaData.getPositions(); + this.stopIndices = areaData.getStopIndices(); + this.shoal = areaData.getShoalType(); + } + + public boolean contains(final WorldPoint wp) + { + return area.contains(wp); + } +} diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalOverlay.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalOverlay.java new file mode 100644 index 00000000..6e0bc217 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalOverlay.java @@ -0,0 +1,258 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.NPC; +import net.runelite.api.Perspective; +import net.runelite.api.Point; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayUtil; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.Stroke; +import java.util.Set; + +@Slf4j +@Singleton +public class ShoalOverlay extends Overlay + implements PluginLifecycleComponent { + + private static final int SHOAL_HIGHLIGHT_SIZE = 10; + + @Nonnull + private final Client client; + private final SailingConfig config; + private final ShoalTracker shoalTracker; + private final NetDepthTimer netDepthTimer; + + @Inject + public ShoalOverlay(@Nonnull Client client, SailingConfig config, ShoalTracker shoalTracker, NetDepthTimer netDepthTimer) { + this.client = client; + this.config = config; + this.shoalTracker = shoalTracker; + this.netDepthTimer = netDepthTimer; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_WIDGETS); + setPriority(PRIORITY_HIGHEST); + } + + @Override + public boolean isEnabled(SailingConfig config) { + return config.trawlingHighlightShoals(); + } + + @Override + public void startUp() { + log.debug("ShoalOverlay starting up"); + } + + @Override + public void shutDown() { + log.debug("ShoalOverlay shutting down"); + } + + @Override + public Dimension render(Graphics2D graphics) { + if (!config.trawlingHighlightShoals()) { + return null; + } + + // Use NPC for highlighting instead of GameObjects for more reliable rendering + NPC shoalNpc = shoalTracker.getCurrentShoalNpc(); + if (shoalNpc != null) { + renderShoalNpcHighlight(graphics, shoalNpc); + renderDepthTimer(graphics, shoalNpc); + return null; + } + + // Fallback to GameObject highlighting if NPC is not available + Set shoals = shoalTracker.getShoalObjects(); + if (!shoals.isEmpty()) { + GameObject shoalToHighlight = selectShoalToHighlight(shoals); + if (shoalToHighlight != null) { + renderShoalHighlight(graphics, shoalToHighlight); + } + } + + return null; + } + + /** + * Select which shoal to highlight when multiple shoals are present. + * Priority: Special shoals (green) > Regular shoals (config color) + */ + private GameObject selectShoalToHighlight(Set shoals) { + GameObject firstSpecialShoal = null; + GameObject firstRegularShoal = null; + + for (GameObject shoal : shoals) { + if (isSpecialShoal(shoal.getId())) { + if (firstSpecialShoal == null) { + firstSpecialShoal = shoal; + } + } else { + if (firstRegularShoal == null) { + firstRegularShoal = shoal; + } + } + + // If we have both types, we can stop looking + if (firstSpecialShoal != null && firstRegularShoal != null) { + break; + } + } + + // Prioritize special shoals over regular ones + return firstSpecialShoal != null ? firstSpecialShoal : firstRegularShoal; + } + + private void renderShoalNpcHighlight(Graphics2D graphics, NPC shoalNpc) { + Polygon poly = Perspective.getCanvasTileAreaPoly(client, shoalNpc.getLocalLocation(), SHOAL_HIGHLIGHT_SIZE); + if (poly != null) { + // Use depth-based coloring for NPC highlighting + Color color = getShoalColorFromDepth(); + Stroke originalStroke = graphics.getStroke(); + graphics.setStroke(new BasicStroke(0.5f)); + OverlayUtil.renderPolygon(graphics, poly, color); + graphics.setStroke(originalStroke); + } + } + + private void renderShoalHighlight(Graphics2D graphics, GameObject shoal) { + Polygon poly = Perspective.getCanvasTileAreaPoly(client, shoal.getLocalLocation(), SHOAL_HIGHLIGHT_SIZE); + if (poly != null) { + Color color = getShoalColor(shoal.getId()); + Stroke originalStroke = graphics.getStroke(); + graphics.setStroke(new BasicStroke(0.5f)); + OverlayUtil.renderPolygon(graphics, poly, color); + graphics.setStroke(originalStroke); + } + } + + private Color getShoalColorFromDepth() { + // Check if we have any special shoal GameObjects + Set shoals = shoalTracker.getShoalObjects(); + boolean hasSpecialShoal = shoals.stream() + .anyMatch(shoal -> isSpecialShoal(shoal.getId())); + + if (hasSpecialShoal) { + log.debug("Special shoal detected, using green highlight"); + return Color.GREEN; + } + + // Use config color for regular shoals + log.debug("Regular shoal detected, using config color"); + return config.trawlingShoalHighlightColour(); + } + + private Color getShoalColor(int objectId) { + if (isSpecialShoal(objectId)) { + return Color.GREEN; + } + return config.trawlingShoalHighlightColour(); + } + + /** + * Check if the shoal is a special type (VIBRANT, GLISTENING, SHIMMERING) + */ + private boolean isSpecialShoal(int objectId) { + return objectId == TrawlingData.ShoalObjectID.VIBRANT || + objectId == TrawlingData.ShoalObjectID.GLISTENING || + objectId == TrawlingData.ShoalObjectID.SHIMMERING; + } + + /** + * Render depth timer text on the shoal NPC + */ + private void renderDepthTimer(Graphics2D graphics, NPC shoalNpc) { + if (!config.trawlingShowNetDepthTimer()) { + return; + } + + NetDepthTimer.TimerInfo timerInfo = netDepthTimer.getTimerInfo(); + if (timerInfo == null) { + return; + } + + Point textLocation = Perspective.getCanvasTextLocation(client, graphics, shoalNpc.getLocalLocation(), getTimerText(timerInfo), 0); + if (textLocation != null) { + renderTimerText(graphics, textLocation, timerInfo); + } + } + + + + /** + * Get the text to display for the timer + */ + private String getTimerText(NetDepthTimer.TimerInfo timerInfo) { + if (timerInfo.isActive()) { + int ticksUntilChange = timerInfo.getTicksUntilDepthChange(); + return String.valueOf(ticksUntilChange); + } + return null; + } + + /** + * Render the timer text with appropriate styling + */ + private void renderTimerText(Graphics2D graphics, Point textLocation, NetDepthTimer.TimerInfo timerInfo) { + Font originalFont = graphics.getFont(); + Color originalColor = graphics.getColor(); + + // Set font and color + graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 14)); + + Color textColor; + if (!timerInfo.isActive()) { + textColor = timerInfo.isWaiting() ? Color.ORANGE : Color.YELLOW; + } else { + int ticksUntilChange = timerInfo.getTicksUntilDepthChange(); + textColor = ticksUntilChange <= 5 ? Color.RED : Color.WHITE; + } + + String text = getTimerText(timerInfo); + + // Draw text with black outline for better visibility + FontMetrics fm = graphics.getFontMetrics(); + int textWidth = fm.stringWidth(text); + int textHeight = fm.getHeight(); + + // Center the text + int x = textLocation.getX() - textWidth / 2; + int y = textLocation.getY() + textHeight / 4; + + // Draw black outline + graphics.setColor(Color.BLACK); + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + if (dx != 0 || dy != 0) { + graphics.drawString(text, x + dx, y + dy); + } + } + } + + // Draw main text + graphics.setColor(textColor); + graphics.drawString(text, x, y); + + // Restore original font and color + graphics.setFont(originalFont); + graphics.setColor(originalColor); + } + +} diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/BluefinBuccaneersHaven.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/BluefinBuccaneersHaven.java new file mode 100644 index 00000000..4d594684 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/BluefinBuccaneersHaven.java @@ -0,0 +1,163 @@ +// ======================================== +// Shoal Area Export +// ======================================== +// Shoal: Bluefin +// Shoal ID: 59738 +// Generated: 2025-12-21 16:21:01 +// Total waypoints: 227 +// Stop points: 12 +// Stop duration: Retrieved from Shoal.BLUEFIN.getStopDuration() +// ======================================== + +package com.duckblade.osrs.sailing.features.trawling.ShoalPathData; + +import com.duckblade.osrs.sailing.features.trawling.Shoal; +import com.duckblade.osrs.sailing.features.trawling.ShoalAreaData; +import com.duckblade.osrs.sailing.features.trawling.ShoalWaypoint; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; + +/** + * Shoal area definition for Bluefin (ID: 59738) + * Contains waypoint path and area bounds. + * Stop duration is retrieved from the Shoal enum. + * Generated by ShoalPathTracker on 2025-12-21 16:21:01 + */ +public class BluefinBuccaneersHaven implements ShoalAreaData { + + /** Area bounds for this shoal region */ + public static final WorldArea AREA = new WorldArea(1962, 3590, 312, 202, 0); + + /** Shoal type for this area */ + public static final Shoal SHOAL_TYPE = Shoal.BLUEFIN; + + /** Complete waypoint path with stop point information */ + public static final ShoalWaypoint[] WAYPOINTS = { + new ShoalWaypoint(new WorldPoint(2215, 3684, 0), true), + new ShoalWaypoint(new WorldPoint(2218, 3668, 0), false), + new ShoalWaypoint(new WorldPoint(2225, 3665, 0), false), + new ShoalWaypoint(new WorldPoint(2255, 3658, 0), false), + new ShoalWaypoint(new WorldPoint(2260, 3651, 0), false), + new ShoalWaypoint(new WorldPoint(2260, 3635, 0), true), + new ShoalWaypoint(new WorldPoint(2249, 3621, 0), false), + new ShoalWaypoint(new WorldPoint(2218, 3621, 0), false), + new ShoalWaypoint(new WorldPoint(2207, 3630, 0), false), + new ShoalWaypoint(new WorldPoint(2206, 3644, 0), false), + new ShoalWaypoint(new WorldPoint(2196, 3649, 0), true), + new ShoalWaypoint(new WorldPoint(2181, 3641, 0), false), + new ShoalWaypoint(new WorldPoint(2181, 3610, 0), false), + new ShoalWaypoint(new WorldPoint(2174, 3604, 0), false), + new ShoalWaypoint(new WorldPoint(2162, 3604, 0), true), + new ShoalWaypoint(new WorldPoint(2138, 3603, 0), false), + new ShoalWaypoint(new WorldPoint(2131, 3600, 0), false), + new ShoalWaypoint(new WorldPoint(2100, 3600, 0), false), + new ShoalWaypoint(new WorldPoint(2072, 3611, 0), false), + new ShoalWaypoint(new WorldPoint(2068, 3621, 0), true), + new ShoalWaypoint(new WorldPoint(2061, 3624, 0), false), + new ShoalWaypoint(new WorldPoint(2045, 3626, 0), false), + new ShoalWaypoint(new WorldPoint(2035, 3634, 0), false), + new ShoalWaypoint(new WorldPoint(2027, 3634, 0), true), + new ShoalWaypoint(new WorldPoint(2009, 3635, 0), false), + new ShoalWaypoint(new WorldPoint(1985, 3659, 0), false), + new ShoalWaypoint(new WorldPoint(1972, 3668, 0), false), + new ShoalWaypoint(new WorldPoint(1983, 3700, 0), false), + new ShoalWaypoint(new WorldPoint(2001, 3713, 0), false), + new ShoalWaypoint(new WorldPoint(2028, 3713, 0), true), + new ShoalWaypoint(new WorldPoint(2036, 3721, 0), false), + new ShoalWaypoint(new WorldPoint(2034, 3730, 0), false), + new ShoalWaypoint(new WorldPoint(2010, 3750, 0), false), + new ShoalWaypoint(new WorldPoint(2007, 3754, 0), false), + new ShoalWaypoint(new WorldPoint(2007, 3759, 0), true), + new ShoalWaypoint(new WorldPoint(2015, 3777, 0), false), + new ShoalWaypoint(new WorldPoint(2027, 3782, 0), false), + new ShoalWaypoint(new WorldPoint(2038, 3775, 0), false), + new ShoalWaypoint(new WorldPoint(2057, 3750, 0), false), + new ShoalWaypoint(new WorldPoint(2075, 3748, 0), true), + new ShoalWaypoint(new WorldPoint(2077, 3746, 0), false), + new ShoalWaypoint(new WorldPoint(2092, 3746, 0), false), + new ShoalWaypoint(new WorldPoint(2106, 3733, 0), false), + new ShoalWaypoint(new WorldPoint(2092, 3713, 0), false), + new ShoalWaypoint(new WorldPoint(2112, 3712, 0), true), + new ShoalWaypoint(new WorldPoint(2144, 3711, 0), false), + new ShoalWaypoint(new WorldPoint(2169, 3719, 0), false), + new ShoalWaypoint(new WorldPoint(2175, 3728, 0), false), + new ShoalWaypoint(new WorldPoint(2175, 3738, 0), true), + new ShoalWaypoint(new WorldPoint(2182, 3760, 0), false), + new ShoalWaypoint(new WorldPoint(2187, 3763, 0), false), + new ShoalWaypoint(new WorldPoint(2201, 3763, 0), false), + new ShoalWaypoint(new WorldPoint(2208, 3759, 0), false), + new ShoalWaypoint(new WorldPoint(2216, 3758, 0), false), + new ShoalWaypoint(new WorldPoint(2229, 3752, 0), false), + new ShoalWaypoint(new WorldPoint(2250, 3730, 0), true), + new ShoalWaypoint(new WorldPoint(2264, 3715, 0), false), + new ShoalWaypoint(new WorldPoint(2259, 3709, 0), false), + new ShoalWaypoint(new WorldPoint(2249, 3704, 0), false), + new ShoalWaypoint(new WorldPoint(2228, 3704, 0), false), + new ShoalWaypoint(new WorldPoint(2215, 3691, 0), false), + new ShoalWaypoint(new WorldPoint(2215, 3684, 0), false), + }; + + // Singleton instance for interface access + public static final BluefinBuccaneersHaven INSTANCE = new BluefinBuccaneersHaven(); + + private BluefinBuccaneersHaven() {} // Private constructor + + // Interface implementations + @Override + public WorldArea getArea() { return AREA; } + + @Override + public ShoalWaypoint[] getWaypoints() { return WAYPOINTS; } + + @Override + public Shoal getShoalType() { return SHOAL_TYPE; } +} + +// ======================================== +// Integration with ShoalFishingArea enum +// ======================================== +// Add this entry to ShoalFishingArea enum: +/* +BLUEFIN_AREA(ShoalBluefinArea.INSTANCE), +*/ + +// ======================================== +// Usage Examples +// ======================================== +// Check if player is in area: +// boolean inArea = ShoalBluefinArea.INSTANCE.contains(playerLocation); + +// Get waypoints for rendering: +// WorldPoint[] path = ShoalBluefinArea.INSTANCE.getPositions(); + +// Get stop duration (from Shoal enum): +// int duration = ShoalBluefinArea.INSTANCE.getStopDuration(); + +// Access static fields directly: +// WorldArea area = ShoalBluefinArea.AREA; +// ShoalWaypoint[] waypoints = ShoalBluefinArea.WAYPOINTS; +// Shoal shoalType = ShoalBluefinArea.SHOAL_TYPE; + +// ======================================== +// Analysis Data +// ======================================== +// Area bounds: 1962, 3590, 312, 202 +// Stop points: 12 total +// Stop duration: Retrieved from BLUEFIN shoal type +// Stop point details: +// Stop 1 (index 0): WorldPoint(x=2215, y=3684, plane=0) +// Stop 2 (index 15): WorldPoint(x=2260, y=3635, plane=0) +// Stop 3 (index 34): WorldPoint(x=2196, y=3649, plane=0) +// Stop 4 (index 50): WorldPoint(x=2162, y=3604, plane=0) +// Stop 5 (index 70): WorldPoint(x=2068, y=3621, plane=0) +// Stop 6 (index 80): WorldPoint(x=2027, y=3634, plane=0) +// Stop 7 (index 103): WorldPoint(x=2028, y=3713, plane=0) +// Stop 8 (index 121): WorldPoint(x=2007, y=3759, plane=0) +// Stop 9 (index 154): WorldPoint(x=2075, y=3748, plane=0) +// Stop 10 (index 171): WorldPoint(x=2112, y=3712, plane=0) +// Stop 11 (index 184): WorldPoint(x=2175, y=3738, plane=0) +// Stop 12 (index 210): WorldPoint(x=2250, y=3730, plane=0) + +// ======================================== +// End of Export +// ======================================== diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/BluefinRainbowReef.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/BluefinRainbowReef.java new file mode 100644 index 00000000..fa6c9a32 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/BluefinRainbowReef.java @@ -0,0 +1,104 @@ +// ======================================== +// Shoal Area Export +// ======================================== +// Shoal: Bluefin +// Shoal ID: 59738 +// Generated: 2025-12-21 03:06:34 +// Total waypoints: 204 +// Stop points: 10 +// Area-based stop duration: 70 ticks +// ======================================== + +package com.duckblade.osrs.sailing.features.trawling.ShoalPathData; + +import com.duckblade.osrs.sailing.features.trawling.Shoal; +import com.duckblade.osrs.sailing.features.trawling.ShoalAreaData; +import com.duckblade.osrs.sailing.features.trawling.ShoalWaypoint; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; + +/** + * Shoal area definition for Bluefin (ID: 59738) + * Contains waypoint path, area bounds, and stop duration. + * Generated by ShoalPathTracker on 2025-12-21 03:06:34 + */ +public class BluefinRainbowReef implements ShoalAreaData { + + public static final WorldArea AREA = new WorldArea(2099, 2211, 287, 190, 0); + public static final Shoal SHOAL_TYPE = Shoal.BLUEFIN; + + public static final ShoalWaypoint[] WAYPOINTS = { + new ShoalWaypoint(new WorldPoint(2153, 2337, 0), true), + new ShoalWaypoint(new WorldPoint(2185, 2367, 0), false), + new ShoalWaypoint(new WorldPoint(2201, 2374, 0), false), + new ShoalWaypoint(new WorldPoint(2202, 2385, 0), false), + new ShoalWaypoint(new WorldPoint(2195, 2391, 0), false), + new ShoalWaypoint(new WorldPoint(2164, 2391, 0), false), + new ShoalWaypoint(new WorldPoint(2147, 2384, 0), true), + new ShoalWaypoint(new WorldPoint(2141, 2376, 0), false), + new ShoalWaypoint(new WorldPoint(2115, 2361, 0), false), + new ShoalWaypoint(new WorldPoint(2113, 2358, 0), false), + new ShoalWaypoint(new WorldPoint(2113, 2336, 0), false), + new ShoalWaypoint(new WorldPoint(2136, 2321, 0), true), + new ShoalWaypoint(new WorldPoint(2139, 2317, 0), false), + new ShoalWaypoint(new WorldPoint(2137, 2286, 0), false), + new ShoalWaypoint(new WorldPoint(2133, 2282, 0), false), + new ShoalWaypoint(new WorldPoint(2124, 2281, 0), false), + new ShoalWaypoint(new WorldPoint(2109, 2262, 0), false), + new ShoalWaypoint(new WorldPoint(2109, 2248, 0), true), + new ShoalWaypoint(new WorldPoint(2115, 2235, 0), false), + new ShoalWaypoint(new WorldPoint(2119, 2233, 0), false), + new ShoalWaypoint(new WorldPoint(2140, 2236, 0), false), + new ShoalWaypoint(new WorldPoint(2159, 2262, 0), false), + new ShoalWaypoint(new WorldPoint(2173, 2262, 0), false), + new ShoalWaypoint(new WorldPoint(2183, 2256, 0), false), + new ShoalWaypoint(new WorldPoint(2185, 2246, 0), true), + new ShoalWaypoint(new WorldPoint(2189, 2226, 0), false), + new ShoalWaypoint(new WorldPoint(2195, 2221, 0), false), + new ShoalWaypoint(new WorldPoint(2227, 2221, 0), false), + new ShoalWaypoint(new WorldPoint(2252, 2224, 0), false), + new ShoalWaypoint(new WorldPoint(2256, 2230, 0), false), + new ShoalWaypoint(new WorldPoint(2258, 2251, 0), false), + new ShoalWaypoint(new WorldPoint(2267, 2257, 0), false), + new ShoalWaypoint(new WorldPoint(2271, 2257, 0), true), + new ShoalWaypoint(new WorldPoint(2299, 2256, 0), false), + new ShoalWaypoint(new WorldPoint(2320, 2235, 0), false), + new ShoalWaypoint(new WorldPoint(2347, 2235, 0), false), + new ShoalWaypoint(new WorldPoint(2363, 2243, 0), false), + new ShoalWaypoint(new WorldPoint(2376, 2259, 0), false), + new ShoalWaypoint(new WorldPoint(2376, 2267, 0), true), + new ShoalWaypoint(new WorldPoint(2375, 2278, 0), false), + new ShoalWaypoint(new WorldPoint(2362, 2294, 0), false), + new ShoalWaypoint(new WorldPoint(2343, 2303, 0), false), + new ShoalWaypoint(new WorldPoint(2338, 2303, 0), true), + new ShoalWaypoint(new WorldPoint(2316, 2304, 0), false), + new ShoalWaypoint(new WorldPoint(2311, 2305, 0), false), + new ShoalWaypoint(new WorldPoint(2296, 2293, 0), false), + new ShoalWaypoint(new WorldPoint(2280, 2288, 0), false), + new ShoalWaypoint(new WorldPoint(2269, 2277, 0), false), + new ShoalWaypoint(new WorldPoint(2258, 2272, 0), true), + new ShoalWaypoint(new WorldPoint(2238, 2272, 0), false), + new ShoalWaypoint(new WorldPoint(2233, 2275, 0), false), + new ShoalWaypoint(new WorldPoint(2201, 2275, 0), false), + new ShoalWaypoint(new WorldPoint(2199, 2275, 0), true), + new ShoalWaypoint(new WorldPoint(2168, 2275, 0), false), + new ShoalWaypoint(new WorldPoint(2155, 2279, 0), false), + new ShoalWaypoint(new WorldPoint(2147, 2292, 0), false), + new ShoalWaypoint(new WorldPoint(2147, 2322, 0), false), + new ShoalWaypoint(new WorldPoint(2153, 2337, 0), false), + }; + + public static final BluefinRainbowReef INSTANCE = new BluefinRainbowReef(); + + private BluefinRainbowReef() {} + + @Override + public WorldArea getArea() { return AREA; } + + @Override + public ShoalWaypoint[] getWaypoints() { return WAYPOINTS; } + + @Override + public Shoal getShoalType() { return SHOAL_TYPE; } + +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/HalibutPortRoberts.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/HalibutPortRoberts.java new file mode 100644 index 00000000..d1b25e54 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/HalibutPortRoberts.java @@ -0,0 +1,156 @@ +// ======================================== +// Shoal Area Export +// ======================================== +// Shoal: Halibut +// Shoal ID: 59737 +// Generated: 2025-12-21 16:48:24 +// Total waypoints: 229 +// Stop points: 8 +// Stop duration: Retrieved from Shoal.HALIBUT.getStopDuration() +// ======================================== + +package com.duckblade.osrs.sailing.features.trawling.ShoalPathData; + +import com.duckblade.osrs.sailing.features.trawling.Shoal; +import com.duckblade.osrs.sailing.features.trawling.ShoalAreaData; +import com.duckblade.osrs.sailing.features.trawling.ShoalWaypoint; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; + +/** + * Shoal area definition for Halibut (ID: 59737) + * Contains waypoint path and area bounds. + * Stop duration is retrieved from the Shoal enum. + * Generated by ShoalPathTracker on 2025-12-21 16:48:24 + */ +public class HalibutPortRoberts implements ShoalAreaData { + + /** Area bounds for this shoal region */ + public static final WorldArea AREA = new WorldArea(1821, 3120, 211, 300, 0); + + /** Shoal type for this area */ + public static final Shoal SHOAL_TYPE = Shoal.HALIBUT; + + /** Complete waypoint path with stop point information */ + + public static final ShoalWaypoint[] WAYPOINTS = { + new ShoalWaypoint(new WorldPoint(1845, 3290, 0), true), + new ShoalWaypoint(new WorldPoint(1847, 3317, 0), false), + new ShoalWaypoint(new WorldPoint(1863, 3330, 0), false), + new ShoalWaypoint(new WorldPoint(1885, 3339, 0), false), + new ShoalWaypoint(new WorldPoint(1898, 3368, 0), false), + new ShoalWaypoint(new WorldPoint(1906, 3377, 0), false), + new ShoalWaypoint(new WorldPoint(1908, 3376, 0), true), + new ShoalWaypoint(new WorldPoint(1916, 3381, 0), false), + new ShoalWaypoint(new WorldPoint(1915, 3386, 0), false), + new ShoalWaypoint(new WorldPoint(1906, 3396, 0), false), + new ShoalWaypoint(new WorldPoint(1915, 3408, 0), false), + new ShoalWaypoint(new WorldPoint(1919, 3410, 0), false), + new ShoalWaypoint(new WorldPoint(1951, 3410, 0), false), + new ShoalWaypoint(new WorldPoint(1963, 3408, 0), true), + new ShoalWaypoint(new WorldPoint(1965, 3403, 0), false), + new ShoalWaypoint(new WorldPoint(1965, 3378, 0), false), + new ShoalWaypoint(new WorldPoint(1974, 3366, 0), false), + new ShoalWaypoint(new WorldPoint(1979, 3363, 0), false), + new ShoalWaypoint(new WorldPoint(2001, 3363, 0), false), + new ShoalWaypoint(new WorldPoint(2015, 3352, 0), false), + new ShoalWaypoint(new WorldPoint(2018, 3321, 0), false), + new ShoalWaypoint(new WorldPoint(2022, 3313, 0), false), + new ShoalWaypoint(new WorldPoint(2022, 3296, 0), true), + new ShoalWaypoint(new WorldPoint(2016, 3287, 0), false), + new ShoalWaypoint(new WorldPoint(2000, 3280, 0), false), + new ShoalWaypoint(new WorldPoint(2002, 3262, 0), false), + new ShoalWaypoint(new WorldPoint(2006, 3255, 0), false), + new ShoalWaypoint(new WorldPoint(2004, 3234, 0), true), + new ShoalWaypoint(new WorldPoint(1989, 3235, 0), false), + new ShoalWaypoint(new WorldPoint(1979, 3240, 0), false), + new ShoalWaypoint(new WorldPoint(1971, 3232, 0), false), + new ShoalWaypoint(new WorldPoint(1986, 3206, 0), false), + new ShoalWaypoint(new WorldPoint(1986, 3176, 0), false), + new ShoalWaypoint(new WorldPoint(1988, 3160, 0), true), + new ShoalWaypoint(new WorldPoint(1993, 3138, 0), false), + new ShoalWaypoint(new WorldPoint(1985, 3131, 0), false), + new ShoalWaypoint(new WorldPoint(1970, 3140, 0), false), + new ShoalWaypoint(new WorldPoint(1940, 3140, 0), false), + new ShoalWaypoint(new WorldPoint(1911, 3140, 0), true), + new ShoalWaypoint(new WorldPoint(1907, 3142, 0), false), + new ShoalWaypoint(new WorldPoint(1908, 3148, 0), false), + new ShoalWaypoint(new WorldPoint(1933, 3164, 0), false), + new ShoalWaypoint(new WorldPoint(1935, 3210, 0), false), + new ShoalWaypoint(new WorldPoint(1929, 3217, 0), false), + new ShoalWaypoint(new WorldPoint(1923, 3220, 0), false), + new ShoalWaypoint(new WorldPoint(1910, 3220, 0), true), + new ShoalWaypoint(new WorldPoint(1894, 3212, 0), false), + new ShoalWaypoint(new WorldPoint(1879, 3198, 0), false), + new ShoalWaypoint(new WorldPoint(1857, 3198, 0), false), + new ShoalWaypoint(new WorldPoint(1843, 3214, 0), true), + new ShoalWaypoint(new WorldPoint(1846, 3238, 0), false), + new ShoalWaypoint(new WorldPoint(1873, 3256, 0), false), + new ShoalWaypoint(new WorldPoint(1872, 3261, 0), false), + new ShoalWaypoint(new WorldPoint(1861, 3263, 0), false), + new ShoalWaypoint(new WorldPoint(1849, 3257, 0), false), + new ShoalWaypoint(new WorldPoint(1834, 3257, 0), false), + new ShoalWaypoint(new WorldPoint(1832, 3266, 0), false), + new ShoalWaypoint(new WorldPoint(1845, 3282, 0), false), + }; + + // Singleton instance for interface access + public static final HalibutPortRoberts INSTANCE = new HalibutPortRoberts(); + + private HalibutPortRoberts() {} // Private constructor + + // Interface implementations + @Override + public WorldArea getArea() { return AREA; } + + @Override + public ShoalWaypoint[] getWaypoints() { return WAYPOINTS; } + + @Override + public Shoal getShoalType() { return SHOAL_TYPE; } +} + +// ======================================== +// Integration with ShoalFishingArea enum +// ======================================== +// Add this entry to ShoalFishingArea enum: +/* +HALIBUT_AREA(ShoalHalibutArea.INSTANCE), +*/ + +// ======================================== +// Usage Examples +// ======================================== +// Check if player is in area: +// boolean inArea = ShoalHalibutArea.INSTANCE.contains(playerLocation); + +// Get waypoints for rendering: +// WorldPoint[] path = ShoalHalibutArea.INSTANCE.getPositions(); + +// Get stop duration (from Shoal enum): +// int duration = ShoalHalibutArea.INSTANCE.getStopDuration(); + +// Access static fields directly: +// WorldArea area = ShoalHalibutArea.AREA; +// ShoalWaypoint[] waypoints = ShoalHalibutArea.WAYPOINTS; +// Shoal shoalType = ShoalHalibutArea.SHOAL_TYPE; + +// ======================================== +// Analysis Data +// ======================================== +// Area bounds: 1821, 3120, 211, 300 +// Stop points: 8 total +// Stop duration: Retrieved from HALIBUT shoal type +// Stop point details: +// Stop 1 (index 0): WorldPoint(x=1845, y=3290, plane=0) +// Stop 2 (index 34): WorldPoint(x=1908, y=3376, plane=0) +// Stop 3 (index 53): WorldPoint(x=1963, y=3408, plane=0) +// Stop 4 (index 74): WorldPoint(x=2022, y=3296, plane=0) +// Stop 5 (index 97): WorldPoint(x=2004, y=3234, plane=0) +// Stop 6 (index 125): WorldPoint(x=1988, y=3160, plane=0) +// Stop 7 (index 146): WorldPoint(x=1911, y=3140, plane=0) +// Stop 8 (index 175): WorldPoint(x=1910, y=3220, plane=0) + +// ======================================== +// End of Export +// ======================================== diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/HalibutSouthernExpanse.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/HalibutSouthernExpanse.java new file mode 100644 index 00000000..0c133a05 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/HalibutSouthernExpanse.java @@ -0,0 +1,116 @@ +// ======================================== +// Shoal Area Export +// ======================================== +// Shoal: Halibut +// Shoal ID: 59737 +// Generated: 2025-12-21 04:14:57 +// Total waypoints: 275 +// Stop points: 10 +// Area-based stop duration: -1 ticks +// ======================================== + +package com.duckblade.osrs.sailing.features.trawling.ShoalPathData; + +import com.duckblade.osrs.sailing.features.trawling.Shoal; +import com.duckblade.osrs.sailing.features.trawling.ShoalAreaData; +import com.duckblade.osrs.sailing.features.trawling.ShoalWaypoint; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; + +/** + * Shoal area definition for Halibut (ID: 59737) + * Contains waypoint path, area bounds, and stop duration. + * Generated by ShoalPathTracker on 2025-12-21 04:14:57 + */ +public class HalibutSouthernExpanse implements ShoalAreaData { + + public static final WorldArea AREA = new WorldArea(1880, 2282, 216, 206, 0); + public static final Shoal SHOAL_TYPE = Shoal.HALIBUT; + + public static final ShoalWaypoint[] WAYPOINTS = { + new ShoalWaypoint(new WorldPoint(1922, 2464, 0), true), + new ShoalWaypoint(new WorldPoint(1910, 2463, 0), false), + new ShoalWaypoint(new WorldPoint(1906, 2458, 0), false), + new ShoalWaypoint(new WorldPoint(1912, 2438, 0), true), + new ShoalWaypoint(new WorldPoint(1914, 2427, 0), false), + new ShoalWaypoint(new WorldPoint(1927, 2420, 0), false), + new ShoalWaypoint(new WorldPoint(1939, 2417, 0), false), + new ShoalWaypoint(new WorldPoint(1938, 2390, 0), false), + new ShoalWaypoint(new WorldPoint(1913, 2375, 0), false), + new ShoalWaypoint(new WorldPoint(1905, 2366, 0), false), + new ShoalWaypoint(new WorldPoint(1905, 2357, 0), true), + new ShoalWaypoint(new WorldPoint(1904, 2341, 0), false), + new ShoalWaypoint(new WorldPoint(1900, 2336, 0), false), + new ShoalWaypoint(new WorldPoint(1890, 2329, 0), false), + new ShoalWaypoint(new WorldPoint(1891, 2315, 0), false), + new ShoalWaypoint(new WorldPoint(1908, 2315, 0), false), + new ShoalWaypoint(new WorldPoint(1920, 2328, 0), false), + new ShoalWaypoint(new WorldPoint(1932, 2328, 0), true), + new ShoalWaypoint(new WorldPoint(1952, 2326, 0), false), + new ShoalWaypoint(new WorldPoint(1966, 2308, 0), false), + new ShoalWaypoint(new WorldPoint(1995, 2294, 0), false), + new ShoalWaypoint(new WorldPoint(2001, 2292, 0), true), + new ShoalWaypoint(new WorldPoint(2030, 2307, 0), false), + new ShoalWaypoint(new WorldPoint(2046, 2314, 0), false), + new ShoalWaypoint(new WorldPoint(2058, 2330, 0), false), + new ShoalWaypoint(new WorldPoint(2054, 2343, 0), false), + new ShoalWaypoint(new WorldPoint(2054, 2373, 0), false), + new ShoalWaypoint(new WorldPoint(2050, 2380, 0), false), + new ShoalWaypoint(new WorldPoint(2037, 2374, 0), true), + new ShoalWaypoint(new WorldPoint(2025, 2377, 0), false), + new ShoalWaypoint(new WorldPoint(2021, 2382, 0), false), + new ShoalWaypoint(new WorldPoint(2023, 2393, 0), false), + new ShoalWaypoint(new WorldPoint(2051, 2384, 0), false), + new ShoalWaypoint(new WorldPoint(2060, 2386, 0), false), + new ShoalWaypoint(new WorldPoint(2061, 2396, 0), false), + new ShoalWaypoint(new WorldPoint(2057, 2404, 0), false), + new ShoalWaypoint(new WorldPoint(2076, 2414, 0), false), + new ShoalWaypoint(new WorldPoint(2086, 2427, 0), false), + new ShoalWaypoint(new WorldPoint(2080, 2436, 0), false), + new ShoalWaypoint(new WorldPoint(2063, 2436, 0), true), + new ShoalWaypoint(new WorldPoint(2032, 2436, 0), false), + new ShoalWaypoint(new WorldPoint(2011, 2428, 0), false), + new ShoalWaypoint(new WorldPoint(1998, 2427, 0), false), + new ShoalWaypoint(new WorldPoint(1990, 2420, 0), false), + new ShoalWaypoint(new WorldPoint(1987, 2414, 0), true), + new ShoalWaypoint(new WorldPoint(2012, 2396, 0), false), + new ShoalWaypoint(new WorldPoint(2021, 2394, 0), false), + new ShoalWaypoint(new WorldPoint(2022, 2386, 0), false), + new ShoalWaypoint(new WorldPoint(2009, 2371, 0), false), + new ShoalWaypoint(new WorldPoint(1979, 2371, 0), false), + new ShoalWaypoint(new WorldPoint(1962, 2375, 0), false), + new ShoalWaypoint(new WorldPoint(1960, 2379, 0), false), + new ShoalWaypoint(new WorldPoint(1960, 2405, 0), true), + new ShoalWaypoint(new WorldPoint(1962, 2419, 0), false), + new ShoalWaypoint(new WorldPoint(1976, 2428, 0), false), + new ShoalWaypoint(new WorldPoint(2005, 2438, 0), false), + new ShoalWaypoint(new WorldPoint(2011, 2448, 0), false), + new ShoalWaypoint(new WorldPoint(2002, 2477, 0), false), + new ShoalWaypoint(new WorldPoint(1987, 2473, 0), false), + new ShoalWaypoint(new WorldPoint(1984, 2470, 0), true), + new ShoalWaypoint(new WorldPoint(1979, 2467, 0), false), + new ShoalWaypoint(new WorldPoint(1968, 2466, 0), false), + new ShoalWaypoint(new WorldPoint(1960, 2453, 0), false), + new ShoalWaypoint(new WorldPoint(1955, 2440, 0), false), + new ShoalWaypoint(new WorldPoint(1950, 2437, 0), false), + new ShoalWaypoint(new WorldPoint(1934, 2439, 0), false), + new ShoalWaypoint(new WorldPoint(1931, 2444, 0), false), + new ShoalWaypoint(new WorldPoint(1929, 2459, 0), false), + new ShoalWaypoint(new WorldPoint(1922, 2464, 0), false), + }; + + public static final HalibutSouthernExpanse INSTANCE = new HalibutSouthernExpanse(); + + private HalibutSouthernExpanse() {} + + @Override + public WorldArea getArea() { return AREA; } + + @Override + public ShoalWaypoint[] getWaypoints() { return WAYPOINTS; } + + @Override + public Shoal getShoalType() { return SHOAL_TYPE; } + +} + diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/MarlinBrittleIsle.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/MarlinBrittleIsle.java new file mode 100644 index 00000000..67219d11 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/MarlinBrittleIsle.java @@ -0,0 +1,148 @@ +// ======================================== +// Shoal Area Export +// ======================================== +// Shoal: Marlin +// Shoal ID: 59739 +// Generated: 2025-12-21 15:45:02 +// Total waypoints: 228 +// Stop points: 8 +// Area-based stop duration: -1 ticks +// ======================================== + +package com.duckblade.osrs.sailing.features.trawling.ShoalPathData; + +import com.duckblade.osrs.sailing.features.trawling.Shoal; +import com.duckblade.osrs.sailing.features.trawling.ShoalAreaData; +import com.duckblade.osrs.sailing.features.trawling.ShoalWaypoint; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; + +/** + * Shoal area definition for Marlin (ID: 59739) + * Contains waypoint path, area bounds, and stop duration. + * Generated by ShoalPathTracker on 2025-12-21 15:45:02 + */ +public class MarlinBrittleIsle implements ShoalAreaData { + + public static final WorldArea AREA = new WorldArea(1856, 3963, 222, 158, 0); + public static final Shoal SHOAL_TYPE = Shoal.MARLIN; + + public static final ShoalWaypoint[] WAYPOINTS = { + new ShoalWaypoint(new WorldPoint(2026, 4087, 0), true), + new ShoalWaypoint(new WorldPoint(2044, 4056, 0), false), + new ShoalWaypoint(new WorldPoint(2058, 4028, 0), false), + new ShoalWaypoint(new WorldPoint(2068, 4008, 0), false), + new ShoalWaypoint(new WorldPoint(2068, 3991, 0), true), + new ShoalWaypoint(new WorldPoint(2055, 3976, 0), false), + new ShoalWaypoint(new WorldPoint(2049, 3973, 0), false), + new ShoalWaypoint(new WorldPoint(2018, 3973, 0), false), + new ShoalWaypoint(new WorldPoint(1986, 3973, 0), false), + new ShoalWaypoint(new WorldPoint(1965, 3973, 0), false), + new ShoalWaypoint(new WorldPoint(1937, 3987, 0), false), + new ShoalWaypoint(new WorldPoint(1909, 4001, 0), false), + new ShoalWaypoint(new WorldPoint(1895, 4008, 0), false), + new ShoalWaypoint(new WorldPoint(1884, 4008, 0), true), + new ShoalWaypoint(new WorldPoint(1878, 4014, 0), false), + new ShoalWaypoint(new WorldPoint(1890, 4042, 0), false), + new ShoalWaypoint(new WorldPoint(1906, 4074, 0), false), + new ShoalWaypoint(new WorldPoint(1921, 4104, 0), false), + new ShoalWaypoint(new WorldPoint(1924, 4109, 0), true), + new ShoalWaypoint(new WorldPoint(1927, 4111, 0), false), + new ShoalWaypoint(new WorldPoint(1951, 4111, 0), false), + new ShoalWaypoint(new WorldPoint(1967, 4104, 0), false), + new ShoalWaypoint(new WorldPoint(1982, 4073, 0), false), + new ShoalWaypoint(new WorldPoint(1998, 4041, 0), false), + new ShoalWaypoint(new WorldPoint(2012, 4013, 0), false), + new ShoalWaypoint(new WorldPoint(2016, 4003, 0), false), + new ShoalWaypoint(new WorldPoint(2016, 3985, 0), true), + new ShoalWaypoint(new WorldPoint(1995, 3973, 0), false), + new ShoalWaypoint(new WorldPoint(1980, 3977, 0), false), + new ShoalWaypoint(new WorldPoint(1953, 3991, 0), false), + new ShoalWaypoint(new WorldPoint(1925, 4005, 0), false), + new ShoalWaypoint(new WorldPoint(1911, 4013, 0), false), + new ShoalWaypoint(new WorldPoint(1902, 4023, 0), false), + new ShoalWaypoint(new WorldPoint(1905, 4032, 0), false), + new ShoalWaypoint(new WorldPoint(1919, 4037, 0), false), + new ShoalWaypoint(new WorldPoint(1932, 4037, 0), true), + new ShoalWaypoint(new WorldPoint(1963, 4037, 0), false), + new ShoalWaypoint(new WorldPoint(1988, 4037, 0), false), + new ShoalWaypoint(new WorldPoint(2016, 4023, 0), false), + new ShoalWaypoint(new WorldPoint(2026, 4018, 0), false), + new ShoalWaypoint(new WorldPoint(2031, 4018, 0), true), + new ShoalWaypoint(new WorldPoint(2049, 4011, 0), false), + new ShoalWaypoint(new WorldPoint(2051, 4005, 0), false), + new ShoalWaypoint(new WorldPoint(2023, 3987, 0), false), + new ShoalWaypoint(new WorldPoint(2016, 3983, 0), false), + new ShoalWaypoint(new WorldPoint(1985, 3983, 0), false), + new ShoalWaypoint(new WorldPoint(1953, 3983, 0), false), + new ShoalWaypoint(new WorldPoint(1928, 3983, 0), true), + new ShoalWaypoint(new WorldPoint(1917, 3986, 0), false), + new ShoalWaypoint(new WorldPoint(1901, 4003, 0), false), + new ShoalWaypoint(new WorldPoint(1887, 4030, 0), false), + new ShoalWaypoint(new WorldPoint(1873, 4059, 0), false), + new ShoalWaypoint(new WorldPoint(1866, 4072, 0), false), + new ShoalWaypoint(new WorldPoint(1866, 4093, 0), false), + new ShoalWaypoint(new WorldPoint(1875, 4108, 0), false), + new ShoalWaypoint(new WorldPoint(1909, 4109, 0), false), + new ShoalWaypoint(new WorldPoint(1941, 4109, 0), false), + new ShoalWaypoint(new WorldPoint(1973, 4109, 0), false), + new ShoalWaypoint(new WorldPoint(2003, 4095, 0), false), + new ShoalWaypoint(new WorldPoint(2021, 4087, 0), false), + }; + + public static final MarlinBrittleIsle INSTANCE = new MarlinBrittleIsle(); + + private MarlinBrittleIsle() {} // Private constructor + + @Override + public WorldArea getArea() { return AREA; } + + @Override + public ShoalWaypoint[] getWaypoints() { return WAYPOINTS; } + + @Override + public Shoal getShoalType() { return SHOAL_TYPE; } +} + +// ======================================== +// Integration with ShoalFishingArea enum +// ======================================== +// Add this entry to ShoalFishingArea enum: +/* +MARLIN_AREA(ShoalMarlinArea.INSTANCE), +*/ + +// ======================================== +// Usage Examples +// ======================================== +// Check if player is in area: +// boolean inArea = ShoalMarlinArea.INSTANCE.contains(playerLocation); + +// Get waypoints for rendering: +// WorldPoint[] path = ShoalMarlinArea.INSTANCE.getPositions(); + +// Get stop duration: +// int duration = ShoalMarlinArea.INSTANCE.getStopDuration(); + +// Access static fields directly: +// WorldArea area = ShoalMarlinArea.AREA; +// ShoalWaypoint[] waypoints = ShoalMarlinArea.WAYPOINTS; + +// ======================================== +// Analysis Data +// ======================================== +// Area bounds: 1856, 3963, 222, 158 +// Stop points: 8 total +// Stop point details: +// Stop 1 (index 0): WorldPoint(x=2026, y=4087, plane=0) +// Stop 2 (index 22): WorldPoint(x=2068, y=3991, plane=0) +// Stop 3 (index 50): WorldPoint(x=1884, y=4008, plane=0) +// Stop 4 (index 76): WorldPoint(x=1924, y=4109, plane=0) +// Stop 5 (index 109): WorldPoint(x=2016, y=3985, plane=0) +// Stop 6 (index 144): WorldPoint(x=1932, y=4037, plane=0) +// Stop 7 (index 158): WorldPoint(x=2031, y=4018, plane=0) +// Stop 8 (index 180): WorldPoint(x=1928, y=3983, plane=0) + +// ======================================== +// End of Export +// ======================================== diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/MarlinWeissmere.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/MarlinWeissmere.java new file mode 100644 index 00000000..1ac627fc --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData/MarlinWeissmere.java @@ -0,0 +1,131 @@ +// ======================================== +// Shoal Area Export +// ======================================== +// Shoal: Vibrant +// Shoal ID: 59742 +// Generated: 2025-12-21 15:32:53 +// Total waypoints: 126 +// Stop points: 8 +// Area-based stop duration: -1 ticks +// ======================================== + +package com.duckblade.osrs.sailing.features.trawling.ShoalPathData; + +import com.duckblade.osrs.sailing.features.trawling.Shoal; +import com.duckblade.osrs.sailing.features.trawling.ShoalAreaData; +import com.duckblade.osrs.sailing.features.trawling.ShoalWaypoint; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; + +/** + * Shoal area definition for Vibrant (ID: 59742) + * Contains waypoint path, area bounds, and stop duration. + * Generated by ShoalPathTracker on 2025-12-21 15:32:53 + */ +public class MarlinWeissmere implements ShoalAreaData { + + public static final WorldArea AREA = new WorldArea(2590, 3945, 278, 201, 0); + public static final Shoal SHOAL_TYPE = Shoal.MARLIN; + + public static final ShoalWaypoint[] WAYPOINTS = { + new ShoalWaypoint(new WorldPoint(2718, 3961, 0), true), + new ShoalWaypoint(new WorldPoint(2713, 3955, 0), false), + new ShoalWaypoint(new WorldPoint(2680, 3955, 0), false), + new ShoalWaypoint(new WorldPoint(2649, 3955, 0), false), + new ShoalWaypoint(new WorldPoint(2642, 3955, 0), false), + new ShoalWaypoint(new WorldPoint(2614, 3968, 0), false), + new ShoalWaypoint(new WorldPoint(2613, 3968, 0), true), + new ShoalWaypoint(new WorldPoint(2600, 3987, 0), false), + new ShoalWaypoint(new WorldPoint(2600, 4019, 0), false), + new ShoalWaypoint(new WorldPoint(2600, 4051, 0), false), + new ShoalWaypoint(new WorldPoint(2600, 4069, 0), true), + new ShoalWaypoint(new WorldPoint(2602, 4074, 0), false), + new ShoalWaypoint(new WorldPoint(2624, 4096, 0), false), + new ShoalWaypoint(new WorldPoint(2646, 4116, 0), false), + new ShoalWaypoint(new WorldPoint(2675, 4114, 0), false), + new ShoalWaypoint(new WorldPoint(2691, 4084, 0), false), + new ShoalWaypoint(new WorldPoint(2706, 4053, 0), false), + new ShoalWaypoint(new WorldPoint(2708, 4050, 0), false), + new ShoalWaypoint(new WorldPoint(2708, 4018, 0), false), + new ShoalWaypoint(new WorldPoint(2708, 4010, 0), true), + new ShoalWaypoint(new WorldPoint(2730, 3989, 0), false), + new ShoalWaypoint(new WorldPoint(2754, 3978, 0), true), + new ShoalWaypoint(new WorldPoint(2770, 3986, 0), false), + new ShoalWaypoint(new WorldPoint(2792, 4008, 0), false), + new ShoalWaypoint(new WorldPoint(2797, 4011, 0), false), + new ShoalWaypoint(new WorldPoint(2812, 4011, 0), true), + new ShoalWaypoint(new WorldPoint(2845, 4011, 0), false), + new ShoalWaypoint(new WorldPoint(2853, 4011, 0), true), + new ShoalWaypoint(new WorldPoint(2858, 4020, 0), false), + new ShoalWaypoint(new WorldPoint(2837, 4043, 0), false), + new ShoalWaypoint(new WorldPoint(2811, 4070, 0), false), + new ShoalWaypoint(new WorldPoint(2788, 4092, 0), false), + new ShoalWaypoint(new WorldPoint(2757, 4124, 0), false), + new ShoalWaypoint(new WorldPoint(2746, 4135, 0), true), + new ShoalWaypoint(new WorldPoint(2742, 4136, 0), false), + new ShoalWaypoint(new WorldPoint(2733, 4131, 0), false), + new ShoalWaypoint(new WorldPoint(2718, 4114, 0), false), + new ShoalWaypoint(new WorldPoint(2718, 4083, 0), false), + new ShoalWaypoint(new WorldPoint(2718, 4052, 0), false), + new ShoalWaypoint(new WorldPoint(2718, 4020, 0), false), + new ShoalWaypoint(new WorldPoint(2718, 3989, 0), false), + new ShoalWaypoint(new WorldPoint(2718, 3962, 0), false), + }; + + public static final MarlinWeissmere INSTANCE = new MarlinWeissmere(); + + private MarlinWeissmere() {} // Private constructor + + @Override + public WorldArea getArea() { return AREA; } + + @Override + public ShoalWaypoint[] getWaypoints() { return WAYPOINTS; } + + @Override + public Shoal getShoalType() { return SHOAL_TYPE; } + +} + +// ======================================== +// Integration with ShoalFishingArea enum +// ======================================== +// Add this entry to ShoalFishingArea enum: +/* +VIBRANT_AREA(ShoalVibrantArea.INSTANCE), +*/ + +// ======================================== +// Usage Examples +// ======================================== +// Check if player is in area: +// boolean inArea = ShoalVibrantArea.INSTANCE.contains(playerLocation); + +// Get waypoints for rendering: +// WorldPoint[] path = ShoalVibrantArea.INSTANCE.getPositions(); + +// Get stop duration: +// int duration = ShoalVibrantArea.INSTANCE.getStopDuration(); + +// Access static fields directly: +// WorldArea area = ShoalVibrantArea.AREA; +// ShoalWaypoint[] waypoints = ShoalVibrantArea.WAYPOINTS; + +// ======================================== +// Analysis Data +// ======================================== +// Area bounds: 2590, 3945, 278, 201 +// Stop points: 8 total +// Stop point details: +// Stop 1 (index 0): WorldPoint(x=2718, y=3961, plane=0) +// Stop 2 (index 14): WorldPoint(x=2613, y=3968, plane=0) +// Stop 3 (index 25): WorldPoint(x=2600, y=4069, plane=0) +// Stop 4 (index 51): WorldPoint(x=2708, y=4010, plane=0) +// Stop 5 (index 65): WorldPoint(x=2754, y=3978, plane=0) +// Stop 6 (index 73): WorldPoint(x=2812, y=4011, plane=0) +// Stop 7 (index 75): WorldPoint(x=2853, y=4011, plane=0) +// Stop 8 (index 114): WorldPoint(x=2746, y=4135, plane=0) + +// ======================================== +// End of Export +// ======================================== diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathOverlay.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathOverlay.java new file mode 100644 index 00000000..ae007669 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathOverlay.java @@ -0,0 +1,302 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.features.util.BoatTracker; +import com.duckblade.osrs.sailing.features.util.SailingUtil; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import com.google.common.math.IntMath; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.Perspective; +import net.runelite.api.Point; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +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 javax.inject.Singleton; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Stroke; + +@Slf4j +@Singleton +public class ShoalPathOverlay extends Overlay implements PluginLifecycleComponent { + + private final Client client; + private final SailingConfig config; + private final BoatTracker boatTracker; + + public static final int MAX_SPLITTABLE_DISTANCE = 10; + + // Arrow spacing - draw an arrow every N points along the path + private static final int ARROW_SPACING = 10; + // Arrow size in pixels + private static final int ARROW_SIZE = 10; + // Arrow width (how wide the arrowhead is) + + // Color for stop point overlays (red) + private static final Color STOP_POINT_COLOR = Color.RED; + + @Inject + public ShoalPathOverlay( + Client client, + SailingConfig config, + BoatTracker boatTracker + ) + { + this.client = client; + this.config = config; + this.boatTracker = boatTracker; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.UNDER_WIDGETS); + setPriority(PRIORITY_MED); + } + + @Override + public boolean isEnabled(SailingConfig config) { + return config.trawlingShowShoalPaths(); + } + + @Override + public void startUp() { + log.debug("ShoalPathOverlay started"); + } + + @Override + public void shutDown() { + log.debug("ShoalPathOverlay shut down"); + } + + @Override + public Dimension render(Graphics2D graphics) { + if (!SailingUtil.isSailing(client)) { + return null; + } + if (boatTracker.getBoat() == null || boatTracker.getBoat().getFishingNets().isEmpty()) { + return null; + } + + WorldPoint playerLocation = SailingUtil.getTopLevelWorldPoint(client); + + Color pathColor = config.trawlingShoalPathColour(); + + for (final var area : ShoalFishingArea.AREAS) { + if (!area.contains(playerLocation)) { + continue; + } + + renderPath(graphics, area.getPath(), pathColor); + if (config.trawlingShowShoalDirectionArrows()) { + renderDirectionalArrows(graphics, area.getPath(), pathColor); + } + renderStopPoints(graphics, area.getPath(), area.getStopIndices()); + } + + return null; + } + + private void renderPath(Graphics2D graphics, WorldPoint[] path, Color pathColor) { + if (path == null || path.length < 2) { + return; + } + + graphics.setStroke(new BasicStroke(2)); + + WorldPoint previousWorldPoint = null; + for (WorldPoint worldPoint : path) { + if (previousWorldPoint == null) { + previousWorldPoint = worldPoint; + continue; + } + + renderSegment(graphics, previousWorldPoint, worldPoint, pathColor); + previousWorldPoint = worldPoint; + } + + // Draw line back to start to complete the loop; we use dashed stroke to indicate that + Stroke dashed = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, + 0, new float[] {9}, 0); + graphics.setStroke(dashed); + renderSegment(graphics, path[path.length - 1], path[0], pathColor); + } + + private void renderSegment(Graphics2D graphics, WorldPoint worldPoint1, WorldPoint worldPoint2, Color pathColor) { + LocalPoint localPoint1 = LocalPoint.fromWorld(client, worldPoint1); + LocalPoint localPoint2 = LocalPoint.fromWorld(client, worldPoint2); + if (localPoint1 == null || localPoint2 == null) { + renderSplitSegments(graphics, worldPoint1, worldPoint2, pathColor); + return; + } + + Point canvasPoint1 = Perspective.localToCanvas(client, localPoint1, worldPoint1.getPlane()); + Point canvasPoint2 = Perspective.localToCanvas(client, localPoint2, worldPoint1.getPlane()); + if (canvasPoint1 == null || canvasPoint2 == null) { + renderSplitSegments(graphics, worldPoint1, worldPoint2, pathColor); + return; + } + + graphics.setColor(pathColor); + graphics.drawLine( + canvasPoint1.getX(), + canvasPoint1.getY(), + canvasPoint2.getX(), + canvasPoint2.getY() + ); + } + + /** + * Splits the given segment in half and tries to draw both halves. Keeps trying recursively + * until success or segment becomes too short or no longer splittable. + */ + private void renderSplitSegments(Graphics2D graphics, WorldPoint worldPoint1, WorldPoint worldPoint2, Color pathColor) { + int dx = worldPoint2.getX() - worldPoint1.getX(); + int dy = worldPoint2.getY() - worldPoint1.getY(); + + if (Math.hypot(dx, dy) < MAX_SPLITTABLE_DISTANCE) { + return; + } + + int maxSteps = IntMath.gcd(Math.abs(dx), Math.abs(dy)); + if (maxSteps <= 2) { + return; + } + + int midStep = maxSteps / 2; + int midX = worldPoint1.getX() + (dx / maxSteps * midStep); + int midY = worldPoint1.getY() + (dy / maxSteps * midStep); + WorldPoint midPoint = new WorldPoint(midX, midY, worldPoint1.getPlane()); + + renderSegment(graphics, worldPoint1, midPoint, pathColor); + renderSegment(graphics, midPoint, worldPoint2, pathColor); + } + + private void renderDirectionalArrows(Graphics2D graphics, WorldPoint[] path, Color pathColor) { + if (path == null || path.length < 2) { + return; + } + + graphics.setStroke(new BasicStroke(2)); + graphics.setColor(pathColor); + + // Draw arrows at regular intervals along the path + for (int i = 0; i < path.length - 1; i += ARROW_SPACING) { + WorldPoint currentPoint = path[i]; + WorldPoint nextPoint = path[i + 1]; + + renderArrowAtSegment(graphics, currentPoint, nextPoint, pathColor); + } + + // Also draw an arrow for the closing segment (back to start) + if (path.length > 2) { + renderArrowAtSegment(graphics, path[path.length - 1], path[0], pathColor); + } + } + + private void renderArrowAtSegment(Graphics2D graphics, WorldPoint fromPoint, WorldPoint toPoint, Color pathColor) { + LocalPoint localFrom = LocalPoint.fromWorld(client, fromPoint); + LocalPoint localTo = LocalPoint.fromWorld(client, toPoint); + + if (localFrom == null || localTo == null) { + return; + } + + Point canvasFrom = Perspective.localToCanvas(client, localFrom, fromPoint.getPlane()); + Point canvasTo = Perspective.localToCanvas(client, localTo, toPoint.getPlane()); + + if (canvasFrom == null || canvasTo == null) { + return; + } + + // Calculate the midpoint of the segment for arrow placement + int midX = (canvasFrom.getX() + canvasTo.getX()) / 2; + int midY = (canvasFrom.getY() + canvasTo.getY()) / 2; + + // Calculate direction vector + double dx = canvasTo.getX() - canvasFrom.getX(); + double dy = canvasTo.getY() - canvasFrom.getY(); + double length = Math.sqrt(dx * dx + dy * dy); + + if (length < 1) { + return; // Skip if points are too close + } + + // Normalize direction vector + dx /= length; + dy /= length; + + // Draw arrowhead + drawArrowhead(graphics, midX, midY, dx, dy, pathColor); + } + + private void drawArrowhead(Graphics2D graphics, int x, int y, double dirX, double dirY, Color color) { + graphics.setColor(color); + + // Create a filled triangular arrowhead for better visibility + double arrowAngle = Math.PI / 4; // 45 degrees for wider arrowhead + double arrowLength = ARROW_SIZE; + + // Arrow tip point (ahead of the center point) + int tipX = (int) (x + arrowLength * 0.3 * dirX); + int tipY = (int) (y + arrowLength * 0.3 * dirY); + + // Left wing of arrow + double leftX = x - arrowLength * (dirX * Math.cos(arrowAngle) - dirY * Math.sin(arrowAngle)); + double leftY = y - arrowLength * (dirY * Math.cos(arrowAngle) + dirX * Math.sin(arrowAngle)); + + // Right wing of arrow + double rightX = x - arrowLength * (dirX * Math.cos(-arrowAngle) - dirY * Math.sin(-arrowAngle)); + double rightY = y - arrowLength * (dirY * Math.cos(-arrowAngle) + dirX * Math.sin(-arrowAngle)); + + // Create triangle points for filled arrowhead + int[] xPoints = {tipX, (int)leftX, (int)rightX}; + int[] yPoints = {tipY, (int)leftY, (int)rightY}; + + // Fill the arrowhead triangle + graphics.fillPolygon(xPoints, yPoints, 3); + + // Add a darker outline for better visibility + graphics.setColor(color.darker()); + graphics.setStroke(new BasicStroke(1)); + graphics.drawPolygon(xPoints, yPoints, 3); + } + + private void renderStopPoints(Graphics2D graphics, WorldPoint[] path, int[] stopIndices) { + if (path == null || stopIndices == null) { + return; + } + + for (int index : stopIndices) { + if (index >= path.length) { + continue; + } + + WorldPoint stopPoint = path[index]; + renderStopPointArea(graphics, stopPoint); + } + } + + private void renderStopPointArea(Graphics2D graphics, WorldPoint centerPoint) { + // Convert WorldPoint to LocalPoint + LocalPoint localPoint = LocalPoint.fromWorld(client, centerPoint); + if (localPoint == null) { + return; + } + + // Convert to canvas point + Point canvasPoint = Perspective.localToCanvas(client, localPoint, centerPoint.getPlane()); + if (canvasPoint == null) { + return; + } + + // Draw stop point marker - red filled circle with white outline (matches trace rendering) + graphics.setColor(STOP_POINT_COLOR); + graphics.fillOval(canvasPoint.getX() - 5, canvasPoint.getY() - 5, 10, 10); + graphics.setColor(Color.WHITE); + graphics.drawOval(canvasPoint.getX() - 5, canvasPoint.getY() - 5, 10, 10); + } +} diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathTracker.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathTracker.java new file mode 100644 index 00000000..ad8f1b4d --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathTracker.java @@ -0,0 +1,606 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.features.util.SailingUtil; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import com.google.common.math.DoubleMath; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.GameObject; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameTick; +import net.runelite.client.eventbus.Subscribe; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + + +/* + * Tracks the path of moving shoals (Bluefin and Vibrant) for route tracing. + * Update with different shoal IDs to trace other shoals. Enable the tracer in config and + * disable it once a route is fully traced to export the path to logs. + * Note that the GameObject spawns are used to get accurate positions, while the WorldEntity + * is used to track movement over time. Note that the stop points are not always accurate and may + * require some manual adjustment. + */ +@Slf4j +@Singleton +public class ShoalPathTracker implements PluginLifecycleComponent { + private static final int MIN_PATH_POINTS = 2; // Minimum points before we consider it a valid path + private static final int MIN_WAYPOINT_DISTANCE = 1; // World coordinate units (tiles) + private static final int MAX_WAYPOINT_DISTANCE = 30; // World coordinate units (tiles) + private static final int MAX_PLAYER_DISTANCE = 300; // World coordinate units (tiles) + private static final int AREA_MARGIN = 10; // World coordinate units (tiles) + + // Output file configuration + private static final String OUTPUT_DIR = "src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathData"; + private static final String OUTPUT_FILE_PREFIX = ""; + private static final String OUTPUT_FILE_EXTENSION = ".java"; + + private final Client client; + private final ShoalPathTrackerCommand tracerCommand; + private final ShoalTracker shoalTracker; + + // Track the shoal path + @Getter + private ShoalPath currentPath = null; + + private Integer currentShoalId = null; + + @Inject + public ShoalPathTracker(Client client, ShoalPathTrackerCommand tracerCommand, ShoalTracker shoalTracker) { + this.client = client; + this.tracerCommand = tracerCommand; + this.shoalTracker = shoalTracker; + } + + @Override + public boolean isEnabled(SailingConfig config) { + // Enabled via chat command: ::trackroutes + return tracerCommand.isTracingEnabled(); + } + + @Override + public void startUp() { + log.debug("Route tracing enabled"); + } + + @Override + public void shutDown() { + log.debug("Route tracing disabled"); + exportPath(); + currentPath = null; + currentShoalId = null; + } + + private void exportPath() { + if (currentPath == null) { + log.debug("No shoal path to export"); + return; + } + + if (currentPath.hasValidPath()) { + log.debug("Exporting shoal path with {} waypoints", currentPath.getWaypoints().size()); + currentPath.logCompletedPath(); + } else { + log.debug("Path too short to export (need at least {} points, have {})", + MIN_PATH_POINTS, currentPath.getWaypoints().size()); + } + } + + + + private String getShoalName(int objectId) { + if (objectId == TrawlingData.ShoalObjectID.GIANT_KRILL) return "Giant Krill"; + if (objectId == TrawlingData.ShoalObjectID.HADDOCK) return "Haddock"; + if (objectId == TrawlingData.ShoalObjectID.YELLOWFIN) return "Yellowfin"; + if (objectId == TrawlingData.ShoalObjectID.HALIBUT) return "Halibut"; + if (objectId == TrawlingData.ShoalObjectID.BLUEFIN) return "Bluefin"; + if (objectId == TrawlingData.ShoalObjectID.MARLIN) return "Marlin"; + if (objectId == TrawlingData.ShoalObjectID.SHIMMERING) return "Shimmering"; + if (objectId == TrawlingData.ShoalObjectID.GLISTENING) return "Glistening"; + if (objectId == TrawlingData.ShoalObjectID.VIBRANT) return "Vibrant"; + return "Unknown(" + objectId + ")"; + } + + @Subscribe + public void onGameTick(GameTick e) { + if (!shoalTracker.hasShoal()) { + return; + } + + // Initialize path when shoal is first detected + if (currentPath == null && shoalTracker.hasShoal()) { + // Get the first available shoal object to determine type + Set shoalObjects = shoalTracker.getShoalObjects(); + if (!shoalObjects.isEmpty()) { + GameObject firstShoal = shoalObjects.iterator().next(); + int objectId = firstShoal.getId(); + currentPath = new ShoalPath(objectId, shoalTracker); + currentShoalId = objectId; + log.debug("Path tracking initialized for {} (ID: {})", getShoalName(objectId), objectId); + } + } + + if (currentPath == null) { + return; + } + + // Update location from ShoalTracker + shoalTracker.updateLocation(); + WorldPoint currentLocation = shoalTracker.getCurrentLocation(); + + if (currentLocation != null) { + // Check if shoal type changed (e.g., Halibut -> Glistening) + Set shoalObjects = shoalTracker.getShoalObjects(); + if (!shoalObjects.isEmpty()) { + GameObject currentShoal = shoalObjects.iterator().next(); + int objectId = currentShoal.getId(); + if (currentShoalId != null && currentShoalId != objectId) { + log.debug("Shoal changed from {} to {}", getShoalName(currentShoalId), getShoalName(objectId)); + currentShoalId = objectId; + } + } + + currentPath.updatePosition(currentLocation); + } + } + + @Getter + public class ShoalPath { + private final int shoalId; + private final ShoalTracker shoalTracker; + private final LinkedList waypoints = new LinkedList<>(); + private int ticksAtCurrentPosition = 0; + + public ShoalPath(int shoalId, ShoalTracker shoalTracker) { + this.shoalId = shoalId; + this.shoalTracker = shoalTracker; + } + + public void addPosition(WorldPoint position) { + WorldPoint playerLocation = SailingUtil.getTopLevelWorldPoint(client); + boolean isTooFar = !isNearPosition(playerLocation, position, MAX_PLAYER_DISTANCE); + + // First position + if (waypoints.isEmpty()) { + if (!isTooFar) { + waypoints.add(new Waypoint(position, false)); + } + + ticksAtCurrentPosition = 0; + return; + } + + Waypoint lastWaypoint = waypoints.peekLast(); + WorldPoint lastPosition = lastWaypoint.getPosition(); + ticksAtCurrentPosition++; + + // Only add if it's a new position (not too close to last recorded) and + // not a buggy location (from when a shoal turns into a mixed fish shoal) + boolean isTooClose = isNearPosition(lastPosition, position, MIN_WAYPOINT_DISTANCE); + if (isTooClose || isTooFar) { + return; + } + + // Mark previous waypoint as a stop point if we stayed there for 10+ ticks + if (ticksAtCurrentPosition >= 10) { + lastWaypoint.setStopPoint(true); + // Get the actual stationary duration from ShoalTracker + int stationaryDuration = shoalTracker.getStationaryTicks(); + lastWaypoint.setStopDuration(stationaryDuration); + } + + // combine sequential segments with the same slope to reduce number of waypoints + if (!lastWaypoint.isStopPoint() && waypoints.size() >= 2) { + Waypoint penultimateWaypoint = waypoints.get(waypoints.size() - 2); + WorldPoint penultimatePosition = penultimateWaypoint.getPosition(); + + // Use more sophisticated path smoothing to eliminate small zigzags + if (shouldSmoothPath(penultimatePosition, lastPosition, position)) { + waypoints.removeLast(); + } + } + + waypoints.add(new Waypoint(position, false)); + ticksAtCurrentPosition = 0; + } + + public void updatePosition(WorldPoint position) { + addPosition(position); + } + + private boolean isNearPosition(WorldPoint p1, WorldPoint p2, int range) { + int dx = p1.getX() - p2.getX(); + int dy = p1.getY() - p2.getY(); + int distanceSquared = dx * dx + dy * dy; + return distanceSquared < (range * range); + } + + private double getSlope(WorldPoint p1, WorldPoint p2) { + double dx = p1.getX() - p2.getX(); + double dy = p1.getY() - p2.getY(); + return dx / dy; + } + + /** + * Determines if a path segment should be smoothed out to eliminate small zigzags. + * Uses multiple criteria to detect unnecessary waypoints that don't meaningfully + * contribute to following the shoal path. + */ + private boolean shouldSmoothPath(WorldPoint p1, WorldPoint p2, WorldPoint p3) { + // Don't smooth if segment is too long (might be important waypoint) + boolean isSegmentTooLong = !isNearPosition(p2, p1, MAX_WAYPOINT_DISTANCE); + if (isSegmentTooLong) { + return false; + } + + // Check if the three points are nearly collinear (small zigzag) + if (arePointsNearlyCollinear(p1, p2, p3)) { + return true; + } + + // Check if the deviation from direct path is small + if (isSmallDeviation(p1, p2, p3)) { + return true; + } + + // Check if slopes are similar (more conservative than exact match) + double previousSlope = getSlope(p1, p2); + double currentSlope = getSlope(p2, p3); + return DoubleMath.fuzzyEquals(previousSlope, currentSlope, 0.05); // More conservative tolerance + } + + /** + * Checks if three points are nearly collinear using the cross product method. + * Small cross products indicate the points are nearly in a straight line. + */ + private boolean arePointsNearlyCollinear(WorldPoint p1, WorldPoint p2, WorldPoint p3) { + // Calculate cross product of vectors (p1->p2) and (p2->p3) + double dx1 = p2.getX() - p1.getX(); + double dy1 = p2.getY() - p1.getY(); + double dx2 = p3.getX() - p2.getX(); + double dy2 = p3.getY() - p2.getY(); + + double crossProduct = Math.abs(dx1 * dy2 - dy1 * dx2); + + // More conservative threshold - only remove very straight lines + // Reduced from 2.0 to 1.0 for more conservative smoothing + return crossProduct < 1.0; + } + + /** + * Checks if the middle point deviates only slightly from the direct path + * between the first and third points. Small deviations indicate unnecessary waypoints. + */ + private boolean isSmallDeviation(WorldPoint p1, WorldPoint p2, WorldPoint p3) { + // Calculate distance from p2 to the line segment p1-p3 + double distanceToLine = distanceFromPointToLine(p2, p1, p3); + + // More conservative threshold - only remove points very close to the line + // Reduced from 3.0 to 1.5 tiles for more conservative smoothing + return distanceToLine < 1.5; + } + + /** + * Calculates the perpendicular distance from a point to a line segment. + */ + private double distanceFromPointToLine(WorldPoint point, WorldPoint lineStart, WorldPoint lineEnd) { + double dx = lineEnd.getX() - lineStart.getX(); + double dy = lineEnd.getY() - lineStart.getY(); + + // If line segment has zero length, return distance to start point + if (dx == 0 && dy == 0) { + return Math.hypot(point.getX() - lineStart.getX(), point.getY() - lineStart.getY()); + } + + // Calculate the perpendicular distance using the formula: + // distance = |ax + by + c| / sqrt(a² + b²) + // where the line is ax + by + c = 0 + double a = dy; + double b = -dx; + double c = dx * lineStart.getY() - dy * lineStart.getX(); + + return Math.abs(a * point.getX() + b * point.getY() + c) / Math.sqrt(a * a + b * b); + } + + public boolean hasValidPath() { + return waypoints.size() >= MIN_PATH_POINTS + && waypoints.stream().anyMatch(Waypoint::isStopPoint); + } + + public List getWaypoints() { + return Collections.unmodifiableList(waypoints); + } + + public void logCompletedPath() { + // make sure first waypoint is a stop + while (!Objects.requireNonNull(waypoints.peekFirst()).isStopPoint()) { + waypoints.add(waypoints.pop()); + } + + String shoalName = ShoalPathTracker.this.getShoalName(shoalId); + + // Write to file + try { + writePathToFile(shoalName); + ShoalPathTracker.log.info("Shoal path exported to file: {}/{}{}{}", + OUTPUT_DIR, OUTPUT_FILE_PREFIX, shoalId, OUTPUT_FILE_EXTENSION); + } catch (IOException e) { + ShoalPathTracker.log.error("Failed to write path to file", e); + // Fallback to log output + logPathToConsole(shoalName); + } + } + + private void writePathToFile(String shoalName) throws IOException { + // Create output directory if it doesn't exist + Path outputDir = Paths.get(OUTPUT_DIR); + if (!Files.exists(outputDir)) { + Files.createDirectories(outputDir); + } + + // Generate class/enum name + String className = "Shoal" + shoalName.replaceAll("[^A-Za-z0-9]", "") + "Area"; + + // Create output file with timestamp + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")); + String filename = String.format("%s%s%s", OUTPUT_FILE_PREFIX, className, OUTPUT_FILE_EXTENSION); + Path outputFile = outputDir.resolve(filename); + + // Calculate bounds and stop points + int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE; + int minY = Integer.MAX_VALUE, maxY = Integer.MIN_VALUE; + List stopPoints = new ArrayList<>(); + + for (int i = 0; i < waypoints.size(); i++) { + Waypoint wp = waypoints.get(i); + WorldPoint pos = wp.getPosition(); + + minX = Math.min(minX, pos.getX()); + minY = Math.min(minY, pos.getY()); + maxX = Math.max(maxX, pos.getX()); + maxY = Math.max(maxY, pos.getY()); + + if (wp.isStopPoint()) { + stopPoints.add(i); + } + } + + String enumName = shoalName.toUpperCase().replaceAll("[^A-Z0-9]", "_") + "_AREA"; + + // Write to file + try (BufferedWriter writer = Files.newBufferedWriter(outputFile, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + + // File header + writer.write("// ========================================\n"); + writer.write("// Shoal Area Export\n"); + writer.write("// ========================================\n"); + writer.write("// Shoal: " + shoalName + "\n"); + writer.write("// Shoal ID: " + shoalId + "\n"); + writer.write("// Generated: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "\n"); + writer.write("// Total waypoints: " + waypoints.size() + "\n"); + writer.write("// Stop points: " + stopPoints.size() + "\n"); + writer.write("// Stop duration: Retrieved from Shoal." + shoalName.toUpperCase().replace(" ", "_") + ".getStopDuration()\n"); + writer.write("// ========================================\n\n"); + + // Package and imports + writer.write("package com.duckblade.osrs.sailing.features.trawling.ShoalPathData;\n\n"); + writer.write("import com.duckblade.osrs.sailing.features.trawling.Shoal;\n"); + writer.write("import com.duckblade.osrs.sailing.features.trawling.ShoalAreaData;\n"); + writer.write("import com.duckblade.osrs.sailing.features.trawling.ShoalWaypoint;\n"); + writer.write("import net.runelite.api.coords.WorldArea;\n"); + writer.write("import net.runelite.api.coords.WorldPoint;\n\n"); + + // Class definition with complete area data + writer.write("/**\n"); + writer.write(" * Shoal area definition for " + shoalName + " (ID: " + shoalId + ")\n"); + writer.write(" * Contains waypoint path and area bounds.\n"); + writer.write(" * Stop duration is retrieved from the Shoal enum.\n"); + writer.write(" * Generated by ShoalPathTracker on " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "\n"); + writer.write(" */\n"); + writer.write("public class " + className + " implements ShoalAreaData {\n\n"); + + // Area bounds + int areaX = minX - AREA_MARGIN; + int areaY = minY - AREA_MARGIN; + int areaWidth = maxX - minX + 2 * AREA_MARGIN; + int areaHeight = maxY - minY + 2 * AREA_MARGIN; + + writer.write("\t/** Area bounds for this shoal region */\n"); + writer.write(String.format("\tpublic static final WorldArea AREA = new WorldArea(%d, %d, %d, %d, 0);\n\n", + areaX, areaY, areaWidth, areaHeight)); + + // Shoal type + writer.write("\t/** Shoal type for this area */\n"); + writer.write("\tpublic static final Shoal SHOAL_TYPE = Shoal." + shoalName.toUpperCase().replace(" ", "_") + ";\n\n"); + + // Waypoints array + writer.write("\t/** Complete waypoint path with stop point information */\n"); + writer.write("\tpublic static final ShoalWaypoint[] WAYPOINTS = {\n"); + + for (Waypoint wp : waypoints) { + WorldPoint pos = wp.getPosition(); + writer.write(String.format("\t\tnew ShoalWaypoint(new WorldPoint(%d, %d, %d), %s),\n", + pos.getX(), pos.getY(), pos.getPlane(), + wp.isStopPoint() ? "true" : "false")); + } + + writer.write("\t};\n\n"); + + // Singleton instance and interface implementations + writer.write("\t// Singleton instance for interface access\n"); + writer.write("\tpublic static final " + className + " INSTANCE = new " + className + "();\n"); + writer.write("\t\n"); + writer.write("\tprivate " + className + "() {} // Private constructor\n"); + writer.write("\t\n"); + writer.write("\t// Interface implementations\n"); + writer.write("\t@Override\n"); + writer.write("\tpublic WorldArea getArea() { return AREA; }\n"); + writer.write("\t\n"); + writer.write("\t@Override\n"); + writer.write("\tpublic ShoalWaypoint[] getWaypoints() { return WAYPOINTS; }\n"); + writer.write("\t\n"); + writer.write("\t@Override\n"); + writer.write("\tpublic Shoal getShoalType() { return SHOAL_TYPE; }\n"); + + writer.write("}\n\n"); + + // Enum entry for ShoalFishingArea + writer.write("// ========================================\n"); + writer.write("// Integration with ShoalFishingArea enum\n"); + writer.write("// ========================================\n"); + writer.write("// Add this entry to ShoalFishingArea enum:\n"); + writer.write("/*\n"); + writer.write(enumName + "(" + className + ".INSTANCE),\n"); + writer.write("*/\n\n"); + + // Usage examples + writer.write("// ========================================\n"); + writer.write("// Usage Examples\n"); + writer.write("// ========================================\n"); + writer.write("// Check if player is in area:\n"); + writer.write("// boolean inArea = " + className + ".INSTANCE.contains(playerLocation);\n\n"); + writer.write("// Get waypoints for rendering:\n"); + writer.write("// WorldPoint[] path = " + className + ".INSTANCE.getPositions();\n\n"); + writer.write("// Get stop duration (from Shoal enum):\n"); + writer.write("// int duration = " + className + ".INSTANCE.getStopDuration();\n\n"); + writer.write("// Access static fields directly:\n"); + writer.write("// WorldArea area = " + className + ".AREA;\n"); + writer.write("// ShoalWaypoint[] waypoints = " + className + ".WAYPOINTS;\n"); + writer.write("// Shoal shoalType = " + className + ".SHOAL_TYPE;\n\n"); + + // Detailed analysis + writer.write("// ========================================\n"); + writer.write("// Analysis Data\n"); + writer.write("// ========================================\n"); + writer.write("// Area bounds: " + areaX + ", " + areaY + ", " + areaWidth + ", " + areaHeight + "\n"); + writer.write("// Stop points: " + stopPoints.size() + " total\n"); + writer.write("// Stop duration: Retrieved from " + shoalName.toUpperCase().replace(" ", "_") + " shoal type\n"); + + // Stop point details + writer.write("// Stop point details:\n"); + for (int i = 0; i < waypoints.size(); i++) { + Waypoint wp = waypoints.get(i); + if (wp.isStopPoint()) { + int stopNumber = stopPoints.indexOf(i) + 1; + writer.write(String.format("// Stop %d (index %d): %s\n", + stopNumber, i, wp.getPosition())); + } + } + + writer.write("\n// ========================================\n"); + writer.write("// End of Export\n"); + writer.write("// ========================================\n"); + } + } + + private void logPathToConsole(String shoalName) { + // Fallback: log to console in old format + ShoalPathTracker.log.debug("=== SHOAL PATH EXPORT (ID: {}, Name: {}) ===", shoalId, shoalName); + ShoalPathTracker.log.debug("Total waypoints: {}", waypoints.size()); + ShoalPathTracker.log.debug(""); + ShoalPathTracker.log.debug("// Shoal: {} (ID: {}) - Copy this into ShoalPaths.java:", shoalName, shoalId); + ShoalPathTracker.log.debug("public static final WorldPoint[] SHOAL_{}_PATH = {", shoalId); + + int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE; + int minY = Integer.MAX_VALUE, maxY = Integer.MIN_VALUE; + List stopPoints = new ArrayList<>(); + for (int i = 0; i < waypoints.size(); i++) { + Waypoint wp = waypoints.get(i); + WorldPoint pos = wp.getPosition(); + String comment = wp.isStopPoint() ? " // STOP POINT" : ""; + ShoalPathTracker.log.debug(" new WorldPoint({}, {}, {}),{}", + pos.getX(), pos.getY(), pos.getPlane(), comment); + + minX = Math.min(minX, pos.getX()); + minY = Math.min(minY, pos.getY()); + maxX = Math.max(maxX, pos.getX()); + maxY = Math.max(maxY, pos.getY()); + + if (wp.isStopPoint()) { + stopPoints.add(i); + } + } + + ShoalPathTracker.log.debug("};"); + ShoalPathTracker.log.debug(""); + ShoalPathTracker.log.debug("Stop points: {}", waypoints.stream().filter(Waypoint::isStopPoint).count()); + ShoalPathTracker.log.debug(""); + + // Log stop durations for analysis + ShoalPathTracker.log.debug("Stop durations (ticks):"); + for (int i = 0; i < waypoints.size(); i++) { + Waypoint wp = waypoints.get(i); + if (wp.isStopPoint() && wp.getStopDuration() > 0) { + ShoalPathTracker.log.debug(" Stop {} (index {}): {} ticks at {}", + stopPoints.indexOf(i) + 1, i, wp.getStopDuration(), wp.getPosition()); + } + } + + // Calculate average stop duration + List durations = waypoints.stream() + .filter(Waypoint::isStopPoint) + .mapToInt(Waypoint::getStopDuration) + .filter(d -> d > 0) + .boxed() + .collect(Collectors.toList()); + + if (!durations.isEmpty()) { + double avgDuration = durations.stream().mapToInt(Integer::intValue).average().orElse(0.0); + int minDuration = durations.stream().mapToInt(Integer::intValue).min().orElse(0); + int maxDuration = durations.stream().mapToInt(Integer::intValue).max().orElse(0); + ShoalPathTracker.log.debug("Duration stats - Avg: {}, Min: {}, Max: {} ticks", avgDuration, minDuration, maxDuration); + } + else + { + ShoalPathTracker.log.debug("Duration empty, we simply just dont know"); + } + ShoalPathTracker.log.debug(""); + + ShoalPathTracker.log.debug("// Copy this into TrawlingData.java:"); + ShoalPathTracker.log.debug("AREA = {}, {}, {}, {}", + minX - AREA_MARGIN, maxX + AREA_MARGIN, minY - AREA_MARGIN, maxY + AREA_MARGIN + ); + ShoalPathTracker.log.debug("// Copy this into ShoalPathOverlay.java:"); + ShoalPathTracker.log.debug("STOP_INDICES = {};", stopPoints); + ShoalPathTracker.log.debug("====================================="); + } + } + + @Getter + public static class Waypoint { + private final WorldPoint position; + @Setter + private boolean stopPoint; + @Setter + private int stopDuration = 0; // Duration in ticks that shoal was stationary at this point + + public Waypoint(WorldPoint position, boolean stopPoint) { + this.position = position; + this.stopPoint = stopPoint; + } + + } +} diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathTrackerCommand.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathTrackerCommand.java new file mode 100644 index 00000000..19cab498 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathTrackerCommand.java @@ -0,0 +1,93 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.module.ComponentManager; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import com.google.inject.Provider; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ChatMessageType; +import net.runelite.api.events.CommandExecuted; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.chat.QueuedMessage; +import net.runelite.client.eventbus.Subscribe; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +@Slf4j +@Singleton +public class ShoalPathTrackerCommand implements PluginLifecycleComponent { + + private static final String COMMAND_NAME = "trackroutes"; + + private final ChatMessageManager chatMessageManager; + private final Provider componentManagerProvider; + private final boolean developerMode; + @Getter + private boolean tracingEnabled = false; + + @Inject + public ShoalPathTrackerCommand(ChatMessageManager chatMessageManager, Provider componentManagerProvider, @Named("developerMode") boolean developerMode) { + this.chatMessageManager = chatMessageManager; + this.componentManagerProvider = componentManagerProvider; + this.developerMode = developerMode; + } + + @Override + public boolean isEnabled(SailingConfig config) { + // Only available in developer mode + return developerMode; + } + + @Override + public void startUp() { + log.debug("Route tracing command available: ::" + COMMAND_NAME); + } + + @Override + public void shutDown() { + tracingEnabled = false; + log.debug("Route tracing command disabled"); + } + + @Subscribe + public void onCommandExecuted(CommandExecuted commandExecuted) { + if (!COMMAND_NAME.equalsIgnoreCase(commandExecuted.getCommand())) { + return; + } + + String[] arguments = commandExecuted.getArguments(); + + if (arguments.length == 0) { + // Toggle + tracingEnabled = !tracingEnabled; + } else { + // Explicit on/off + String arg = arguments[0].trim().toLowerCase(); + if (arg.equals("on") || arg.equals("true") || arg.equals("1")) { + tracingEnabled = true; + } else if (arg.equals("off") || arg.equals("false") || arg.equals("0")) { + tracingEnabled = false; + } else { + sendChatMessage("Usage: ::trackroutes [on|off] - Current status: " + (tracingEnabled ? "ON" : "OFF")); + return; + } + } + + sendChatMessage("Shoal route tracing is now " + (tracingEnabled ? "ENABLED" : "DISABLED")); + log.debug("Shoal route tracing is now {}", tracingEnabled ? "ENABLED" : "DISABLED"); + + // Trigger component manager to re-check all component states + componentManagerProvider.get().revalidateComponentStates(); + } + + private void sendChatMessage(String message) { + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.CONSOLE) + .value(message) + .build()); + } + +} diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathTrackerOverlay.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathTrackerOverlay.java new file mode 100644 index 00000000..c2d2a065 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPathTrackerOverlay.java @@ -0,0 +1,134 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.Perspective; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.util.List; + + +@Slf4j +@Singleton +public class ShoalPathTrackerOverlay extends Overlay implements PluginLifecycleComponent { + + @Nonnull + private final Client client; + private final SailingConfig config; + private final ShoalPathTracker shoalPathTracker; + private final ShoalPathTrackerCommand tracerCommand; + + @Inject + public ShoalPathTrackerOverlay(@Nonnull Client client, SailingConfig config, ShoalPathTracker shoalPathTracker, ShoalPathTrackerCommand tracerCommand) { + this.client = client; + this.config = config; + this.shoalPathTracker = shoalPathTracker; + this.tracerCommand = tracerCommand; + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + setPriority(PRIORITY_LOW); // Draw paths below the shoal highlights + } + + @Override + public boolean isEnabled(SailingConfig config) { + // Enabled via chat command: ::trackroutes, ::trackroutes on, ::trackroutes off + return tracerCommand.isTracingEnabled(); + } + + @Override + public void startUp() { + log.debug("ShoalPathOverlay started"); + } + + @Override + public void shutDown() { + log.debug("ShoalPathOverlay shut down"); + } + + @Override + public Dimension render(Graphics2D graphics) { + ShoalPathTracker.ShoalPath path = shoalPathTracker.getCurrentPath(); + + if (path == null || !path.hasValidPath()) { + return null; + } + + renderShoalPath(graphics, path); + return null; + } + + private void renderShoalPath(Graphics2D graphics, ShoalPathTracker.ShoalPath path) { + List waypoints = path.getWaypoints(); + if (waypoints.size() < 2) { + return; + } + + // Use in-progress color (yellow) for live tracing + Color pathColor = config.trawlingShoalPathColour(); + + // Draw lines connecting the waypoints + graphics.setStroke(new BasicStroke(1)); + + net.runelite.api.Point previousCanvasPoint = null; + + for (int i = 0; i < waypoints.size(); i++) { + ShoalPathTracker.Waypoint waypoint = waypoints.get(i); + net.runelite.api.coords.WorldPoint worldPos = waypoint.getPosition(); + + // Convert WorldPoint to LocalPoint for rendering + LocalPoint localPos = LocalPoint.fromWorld(client, worldPos); + if (localPos == null) { + previousCanvasPoint = null; + continue; + } + + net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPos, worldPos.getPlane()); + + if (canvasPoint == null) { + previousCanvasPoint = null; + continue; + } + + // Draw line from previous point + if (previousCanvasPoint != null) { + graphics.setColor(pathColor); + graphics.drawLine( + previousCanvasPoint.getX(), + previousCanvasPoint.getY(), + canvasPoint.getX(), + canvasPoint.getY() + ); + } + + // Draw waypoint marker - different colors for stop points + if (waypoint.isStopPoint()) { + // Stop point - draw larger red circle + graphics.setColor(Color.RED); + graphics.fillOval(canvasPoint.getX() - 5, canvasPoint.getY() - 5, 10, 10); + graphics.setColor(Color.WHITE); + graphics.drawOval(canvasPoint.getX() - 5, canvasPoint.getY() - 5, 10, 10); + } else { + // Regular waypoint - small circle in path color + graphics.setColor(pathColor); + graphics.fillOval(canvasPoint.getX() - 3, canvasPoint.getY() - 3, 6, 6); + } + // draw waypoint index + graphics.setColor(Color.GREEN); + graphics.drawString(String.valueOf(i), canvasPoint.getX() - 3, canvasPoint.getY() - 3); + + previousCanvasPoint = canvasPoint; + } + } +} diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPaths.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPaths.java new file mode 100644 index 00000000..392ad3eb --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalPaths.java @@ -0,0 +1,1376 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import net.runelite.api.coords.WorldPoint; + +/** + * Hardcoded shoal paths for various regions. + * These paths are traced using the ShoalPathTracker feature by following shoals with the player's boat. + * To trace a route: + * 1. Launch the client with --developer-mode flag + * 2. Type "::trackroutes on" in chat to enable tracing + * 3. Follow a shoal through its complete loop + * 4. Type "::trackroutes off" in chat to disable and auto-export the traced path to logs + * 5. Copy the exported path from logs into this file + * 6. Add the path to the appropriate overlay to display it + */ +public class ShoalPaths { + + public static final WorldPoint[] GIANT_KRILL_SIMIAN_SEA = { + new WorldPoint(2795, 2559, 0), // STOP POINT + new WorldPoint(2793, 2558, 0), + new WorldPoint(2791, 2558, 0), + new WorldPoint(2789, 2559, 0), + new WorldPoint(2788, 2560, 0), + new WorldPoint(2787, 2562, 0), + new WorldPoint(2787, 2588, 0), // STOP POINT + new WorldPoint(2787, 2601, 0), + new WorldPoint(2784, 2604, 0), + new WorldPoint(2781, 2605, 0), + new WorldPoint(2775, 2608, 0), + new WorldPoint(2765, 2608, 0), + new WorldPoint(2765, 2609, 0), // STOP POINT + new WorldPoint(2765, 2617, 0), + new WorldPoint(2766, 2619, 0), + new WorldPoint(2771, 2624, 0), + new WorldPoint(2775, 2626, 0), + new WorldPoint(2778, 2627, 0), + new WorldPoint(2779, 2628, 0), + new WorldPoint(2781, 2629, 0), + new WorldPoint(2782, 2629, 0), // STOP POINT + new WorldPoint(2787, 2629, 0), + new WorldPoint(2797, 2624, 0), + new WorldPoint(2799, 2624, 0), + new WorldPoint(2801, 2623, 0), + new WorldPoint(2802, 2622, 0), + new WorldPoint(2806, 2614, 0), + new WorldPoint(2806, 2608, 0), // STOP POINT + new WorldPoint(2809, 2602, 0), + new WorldPoint(2813, 2598, 0), + new WorldPoint(2815, 2597, 0), + new WorldPoint(2817, 2597, 0), + new WorldPoint(2827, 2602, 0), + new WorldPoint(2829, 2604, 0), + new WorldPoint(2831, 2605, 0), + new WorldPoint(2833, 2605, 0), + new WorldPoint(2835, 2604, 0), + new WorldPoint(2836, 2603, 0), + new WorldPoint(2839, 2597, 0), + new WorldPoint(2839, 2591, 0), // STOP POINT + new WorldPoint(2840, 2589, 0), + new WorldPoint(2841, 2588, 0), + new WorldPoint(2843, 2587, 0), + new WorldPoint(2845, 2585, 0), + new WorldPoint(2847, 2581, 0), + new WorldPoint(2846, 2580, 0), + new WorldPoint(2845, 2578, 0), + new WorldPoint(2841, 2576, 0), + new WorldPoint(2837, 2576, 0), + new WorldPoint(2829, 2572, 0), // STOP POINT + new WorldPoint(2817, 2566, 0), + new WorldPoint(2804, 2566, 0), + new WorldPoint(2800, 2564, 0), + new WorldPoint(2795, 2559, 0), + }; + + public static final WorldPoint[] GIANT_KRILL_TURTLE_BELT = { + new WorldPoint(2971, 2566, 0), // STOP POINT + new WorldPoint(2979, 2566, 0), + new WorldPoint(2981, 2565, 0), + new WorldPoint(2988, 2558, 0), + new WorldPoint(2988, 2557, 0), + new WorldPoint(2993, 2547, 0), + new WorldPoint(2993, 2531, 0), // STOP POINT + new WorldPoint(2989, 2523, 0), + new WorldPoint(2987, 2521, 0), + new WorldPoint(2986, 2519, 0), + new WorldPoint(2986, 2517, 0), + new WorldPoint(2987, 2515, 0), + new WorldPoint(2988, 2514, 0), + new WorldPoint(2990, 2513, 0), + new WorldPoint(3000, 2513, 0), + new WorldPoint(3012, 2519, 0), + new WorldPoint(3014, 2519, 0), + new WorldPoint(3015, 2518, 0), + new WorldPoint(3016, 2518, 0), + new WorldPoint(3017, 2516, 0), + new WorldPoint(3017, 2512, 0), // STOP POINT + new WorldPoint(3017, 2506, 0), + new WorldPoint(3016, 2504, 0), + new WorldPoint(3016, 2503, 0), + new WorldPoint(3015, 2502, 0), + new WorldPoint(3013, 2501, 0), + new WorldPoint(2987, 2501, 0), + new WorldPoint(2985, 2502, 0), // STOP POINT + new WorldPoint(2983, 2502, 0), + new WorldPoint(2977, 2499, 0), + new WorldPoint(2975, 2499, 0), + new WorldPoint(2974, 2498, 0), + new WorldPoint(2968, 2486, 0), + new WorldPoint(2968, 2478, 0), // STOP POINT + new WorldPoint(2968, 2477, 0), + new WorldPoint(2967, 2476, 0), + new WorldPoint(2967, 2475, 0), + new WorldPoint(2962, 2475, 0), + new WorldPoint(2961, 2476, 0), + new WorldPoint(2961, 2477, 0), + new WorldPoint(2958, 2483, 0), + new WorldPoint(2956, 2483, 0), + new WorldPoint(2954, 2484, 0), + new WorldPoint(2952, 2484, 0), + new WorldPoint(2938, 2477, 0), + new WorldPoint(2937, 2477, 0), + new WorldPoint(2936, 2478, 0), + new WorldPoint(2934, 2479, 0), + new WorldPoint(2933, 2479, 0), + new WorldPoint(2932, 2481, 0), + new WorldPoint(2932, 2485, 0), + new WorldPoint(2939, 2492, 0), + new WorldPoint(2941, 2493, 0), + new WorldPoint(2942, 2494, 0), + new WorldPoint(2943, 2496, 0), + new WorldPoint(2943, 2502, 0), + new WorldPoint(2941, 2506, 0), // STOP POINT + new WorldPoint(2940, 2508, 0), + new WorldPoint(2940, 2515, 0), + new WorldPoint(2941, 2517, 0), + new WorldPoint(2942, 2518, 0), + new WorldPoint(2944, 2519, 0), + new WorldPoint(2946, 2519, 0), + new WorldPoint(2948, 2518, 0), + new WorldPoint(2953, 2518, 0), + new WorldPoint(2961, 2522, 0), + new WorldPoint(2964, 2525, 0), // STOP POINT + new WorldPoint(2970, 2531, 0), + new WorldPoint(2971, 2533, 0), + new WorldPoint(2971, 2535, 0), + new WorldPoint(2970, 2537, 0), + new WorldPoint(2968, 2539, 0), + new WorldPoint(2968, 2540, 0), + new WorldPoint(2967, 2540, 0), + new WorldPoint(2965, 2541, 0), + new WorldPoint(2945, 2541, 0), + new WorldPoint(2944, 2543, 0), + new WorldPoint(2944, 2545, 0), // STOP POINT + new WorldPoint(2944, 2546, 0), + new WorldPoint(2947, 2552, 0), + new WorldPoint(2953, 2558, 0), + new WorldPoint(2963, 2563, 0), + new WorldPoint(2965, 2563, 0), + new WorldPoint(2971, 2566, 0), + }; + + public static final WorldPoint[] GIANT_KRILL_GREAT_SOUND = { + new WorldPoint(1580, 3365, 0), // STOP POINT + new WorldPoint(1583, 3362, 0), + new WorldPoint(1589, 3350, 0), + new WorldPoint(1589, 3347, 0), + new WorldPoint(1591, 3345, 0), + new WorldPoint(1595, 3343, 0), + new WorldPoint(1597, 3341, 0), + new WorldPoint(1598, 3341, 0), + new WorldPoint(1601, 3340, 0), + new WorldPoint(1602, 3339, 0), + new WorldPoint(1605, 3338, 0), + new WorldPoint(1606, 3337, 0), + new WorldPoint(1612, 3337, 0), + new WorldPoint(1614, 3338, 0), + new WorldPoint(1621, 3345, 0), + new WorldPoint(1628, 3359, 0), + new WorldPoint(1628, 3361, 0), + new WorldPoint(1622, 3373, 0), + new WorldPoint(1616, 3379, 0), // STOP POINT + new WorldPoint(1613, 3382, 0), + new WorldPoint(1595, 3391, 0), + new WorldPoint(1587, 3391, 0), + new WorldPoint(1586, 3390, 0), + new WorldPoint(1582, 3388, 0), + new WorldPoint(1581, 3387, 0), + new WorldPoint(1580, 3387, 0), // STOP POINT + new WorldPoint(1578, 3386, 0), + new WorldPoint(1577, 3385, 0), + new WorldPoint(1575, 3384, 0), + new WorldPoint(1563, 3372, 0), + new WorldPoint(1561, 3368, 0), + new WorldPoint(1561, 3364, 0), // STOP POINT + new WorldPoint(1561, 3347, 0), + new WorldPoint(1563, 3343, 0), + new WorldPoint(1565, 3342, 0), + new WorldPoint(1567, 3340, 0), + new WorldPoint(1567, 3339, 0), + new WorldPoint(1569, 3338, 0), + new WorldPoint(1574, 3338, 0), // STOP POINT + new WorldPoint(1575, 3338, 0), + new WorldPoint(1589, 3345, 0), + new WorldPoint(1590, 3346, 0), + new WorldPoint(1591, 3348, 0), + new WorldPoint(1591, 3352, 0), + new WorldPoint(1589, 3356, 0), + new WorldPoint(1588, 3357, 0), + new WorldPoint(1586, 3361, 0), + new WorldPoint(1586, 3367, 0), + new WorldPoint(1582, 3375, 0), + new WorldPoint(1581, 3376, 0), + new WorldPoint(1580, 3379, 0), // STOP POINT + new WorldPoint(1579, 3380, 0), + new WorldPoint(1577, 3381, 0), + new WorldPoint(1567, 3381, 0), + new WorldPoint(1567, 3380, 0), + new WorldPoint(1566, 3379, 0), + new WorldPoint(1566, 3375, 0), + new WorldPoint(1565, 3373, 0), + new WorldPoint(1564, 3372, 0), + new WorldPoint(1560, 3370, 0), // STOP POINT + new WorldPoint(1558, 3371, 0), + new WorldPoint(1557, 3372, 0), + new WorldPoint(1556, 3374, 0), + new WorldPoint(1556, 3376, 0), + new WorldPoint(1557, 3378, 0), + new WorldPoint(1558, 3379, 0), + new WorldPoint(1560, 3380, 0), + new WorldPoint(1562, 3380, 0), + new WorldPoint(1568, 3377, 0), + new WorldPoint(1580, 3365, 0), + }; + + public static final WorldPoint[] YELLOWFIN_THE_CROWN_JEWEL = { + new WorldPoint(1765, 2543, 0), // STOP POINT + new WorldPoint(1785, 2543, 0), + new WorldPoint(1786, 2544, 0), + new WorldPoint(1788, 2545, 0), + new WorldPoint(1791, 2546, 0), + new WorldPoint(1793, 2547, 0), + new WorldPoint(1796, 2548, 0), + new WorldPoint(1798, 2550, 0), + new WorldPoint(1800, 2551, 0), + new WorldPoint(1802, 2551, 0), + new WorldPoint(1803, 2552, 0), + new WorldPoint(1805, 2556, 0), + new WorldPoint(1807, 2558, 0), + new WorldPoint(1808, 2561, 0), + new WorldPoint(1809, 2563, 0), + new WorldPoint(1809, 2578, 0), + new WorldPoint(1808, 2579, 0), + new WorldPoint(1807, 2581, 0), + new WorldPoint(1806, 2584, 0), + new WorldPoint(1805, 2585, 0), + new WorldPoint(1799, 2588, 0), + new WorldPoint(1796, 2589, 0), + new WorldPoint(1766, 2589, 0), + new WorldPoint(1746, 2589, 0), // STOP POINT + new WorldPoint(1726, 2589, 0), + new WorldPoint(1724, 2591, 0), + new WorldPoint(1721, 2592, 0), + new WorldPoint(1717, 2594, 0), + new WorldPoint(1716, 2594, 0), + new WorldPoint(1713, 2597, 0), + new WorldPoint(1711, 2598, 0), + new WorldPoint(1710, 2599, 0), + new WorldPoint(1709, 2601, 0), + new WorldPoint(1708, 2602, 0), + new WorldPoint(1706, 2603, 0), + new WorldPoint(1705, 2604, 0), + new WorldPoint(1704, 2606, 0), + new WorldPoint(1703, 2607, 0), + new WorldPoint(1701, 2608, 0), + new WorldPoint(1700, 2609, 0), + new WorldPoint(1699, 2611, 0), + new WorldPoint(1698, 2612, 0), + new WorldPoint(1697, 2614, 0), + new WorldPoint(1695, 2616, 0), + new WorldPoint(1695, 2617, 0), + new WorldPoint(1694, 2619, 0), + new WorldPoint(1693, 2620, 0), + new WorldPoint(1692, 2623, 0), + new WorldPoint(1689, 2626, 0), + new WorldPoint(1687, 2630, 0), + new WorldPoint(1686, 2633, 0), + new WorldPoint(1686, 2646, 0), + new WorldPoint(1687, 2648, 0), + new WorldPoint(1688, 2651, 0), + new WorldPoint(1691, 2657, 0), + new WorldPoint(1693, 2659, 0), + new WorldPoint(1696, 2660, 0), + new WorldPoint(1698, 2661, 0), + new WorldPoint(1699, 2662, 0), + new WorldPoint(1701, 2661, 0), + new WorldPoint(1704, 2660, 0), // STOP POINT + new WorldPoint(1706, 2659, 0), + new WorldPoint(1709, 2658, 0), + new WorldPoint(1711, 2656, 0), + new WorldPoint(1717, 2656, 0), + new WorldPoint(1719, 2655, 0), + new WorldPoint(1720, 2654, 0), + new WorldPoint(1722, 2653, 0), + new WorldPoint(1723, 2651, 0), + new WorldPoint(1725, 2649, 0), + new WorldPoint(1727, 2648, 0), + new WorldPoint(1728, 2646, 0), + new WorldPoint(1730, 2644, 0), + new WorldPoint(1731, 2641, 0), + new WorldPoint(1733, 2639, 0), + new WorldPoint(1734, 2637, 0), + new WorldPoint(1735, 2634, 0), + new WorldPoint(1741, 2622, 0), + new WorldPoint(1743, 2620, 0), + new WorldPoint(1745, 2619, 0), + new WorldPoint(1760, 2619, 0), // STOP POINT + new WorldPoint(1781, 2619, 0), + new WorldPoint(1782, 2620, 0), + new WorldPoint(1783, 2623, 0), + new WorldPoint(1784, 2625, 0), + new WorldPoint(1786, 2628, 0), + new WorldPoint(1787, 2630, 0), + new WorldPoint(1788, 2633, 0), + new WorldPoint(1789, 2635, 0), + new WorldPoint(1791, 2638, 0), + new WorldPoint(1792, 2640, 0), + new WorldPoint(1792, 2664, 0), + new WorldPoint(1791, 2666, 0), + new WorldPoint(1788, 2669, 0), + new WorldPoint(1786, 2670, 0), + new WorldPoint(1784, 2670, 0), + new WorldPoint(1783, 2671, 0), + new WorldPoint(1781, 2672, 0), + new WorldPoint(1780, 2674, 0), + new WorldPoint(1780, 2675, 0), + new WorldPoint(1781, 2677, 0), // STOP POINT + new WorldPoint(1781, 2704, 0), + new WorldPoint(1782, 2707, 0), + new WorldPoint(1786, 2711, 0), + new WorldPoint(1787, 2713, 0), + new WorldPoint(1787, 2715, 0), + new WorldPoint(1786, 2717, 0), + new WorldPoint(1784, 2719, 0), + new WorldPoint(1780, 2721, 0), + new WorldPoint(1750, 2721, 0), // STOP POINT + new WorldPoint(1748, 2720, 0), + new WorldPoint(1745, 2719, 0), + new WorldPoint(1743, 2717, 0), + new WorldPoint(1740, 2716, 0), + new WorldPoint(1738, 2715, 0), + new WorldPoint(1729, 2706, 0), + new WorldPoint(1727, 2705, 0), + new WorldPoint(1724, 2703, 0), + new WorldPoint(1694, 2703, 0), + new WorldPoint(1692, 2702, 0), + new WorldPoint(1687, 2697, 0), + new WorldPoint(1685, 2693, 0), + new WorldPoint(1684, 2690, 0), + new WorldPoint(1684, 2675, 0), + new WorldPoint(1681, 2669, 0), + new WorldPoint(1672, 2660, 0), + new WorldPoint(1672, 2659, 0), + new WorldPoint(1669, 2656, 0), + new WorldPoint(1668, 2654, 0), // STOP POINT + new WorldPoint(1668, 2638, 0), + new WorldPoint(1667, 2636, 0), + new WorldPoint(1666, 2635, 0), + new WorldPoint(1665, 2633, 0), + new WorldPoint(1659, 2630, 0), + new WorldPoint(1657, 2628, 0), + new WorldPoint(1654, 2627, 0), + new WorldPoint(1652, 2626, 0), + new WorldPoint(1649, 2625, 0), + new WorldPoint(1648, 2625, 0), + new WorldPoint(1646, 2624, 0), + new WorldPoint(1643, 2621, 0), + new WorldPoint(1643, 2616, 0), + new WorldPoint(1644, 2613, 0), + new WorldPoint(1645, 2611, 0), + new WorldPoint(1646, 2608, 0), + new WorldPoint(1647, 2606, 0), + new WorldPoint(1649, 2603, 0), + new WorldPoint(1650, 2601, 0), + new WorldPoint(1651, 2598, 0), + new WorldPoint(1651, 2597, 0), + new WorldPoint(1652, 2595, 0), + new WorldPoint(1653, 2594, 0), + new WorldPoint(1656, 2592, 0), + new WorldPoint(1658, 2591, 0), + new WorldPoint(1662, 2591, 0), // STOP POINT + new WorldPoint(1664, 2590, 0), + new WorldPoint(1665, 2589, 0), + new WorldPoint(1666, 2587, 0), + new WorldPoint(1667, 2584, 0), + new WorldPoint(1667, 2578, 0), + new WorldPoint(1668, 2577, 0), + new WorldPoint(1669, 2575, 0), + new WorldPoint(1670, 2574, 0), + new WorldPoint(1672, 2573, 0), + new WorldPoint(1675, 2571, 0), + new WorldPoint(1679, 2569, 0), + new WorldPoint(1693, 2569, 0), + new WorldPoint(1694, 2570, 0), + new WorldPoint(1695, 2570, 0), + new WorldPoint(1698, 2573, 0), + new WorldPoint(1700, 2574, 0), + new WorldPoint(1703, 2576, 0), + new WorldPoint(1716, 2576, 0), + new WorldPoint(1717, 2575, 0), + new WorldPoint(1719, 2574, 0), + new WorldPoint(1721, 2574, 0), + new WorldPoint(1722, 2573, 0), + new WorldPoint(1723, 2571, 0), + new WorldPoint(1724, 2570, 0), + new WorldPoint(1726, 2569, 0), + new WorldPoint(1728, 2567, 0), + new WorldPoint(1729, 2565, 0), + new WorldPoint(1731, 2564, 0), + new WorldPoint(1732, 2563, 0), + new WorldPoint(1733, 2561, 0), + new WorldPoint(1734, 2560, 0), + new WorldPoint(1735, 2558, 0), + new WorldPoint(1737, 2556, 0), + new WorldPoint(1737, 2555, 0), + new WorldPoint(1738, 2553, 0), + new WorldPoint(1745, 2546, 0), + new WorldPoint(1748, 2545, 0), + new WorldPoint(1750, 2543, 0) + }; + + public static final WorldPoint[] HADDOCK_MISTY_SEA = { + new WorldPoint(1521, 2758, 0), // STOP POINT + new WorldPoint(1524, 2752, 0), + new WorldPoint(1525, 2751, 0), + new WorldPoint(1526, 2748, 0), + new WorldPoint(1527, 2746, 0), + new WorldPoint(1534, 2739, 0), + new WorldPoint(1534, 2738, 0), + new WorldPoint(1535, 2736, 0), + new WorldPoint(1537, 2734, 0), + new WorldPoint(1538, 2732, 0), + new WorldPoint(1538, 2726, 0), + new WorldPoint(1539, 2724, 0), + new WorldPoint(1540, 2723, 0), + new WorldPoint(1544, 2721, 0), + new WorldPoint(1549, 2721, 0), + new WorldPoint(1571, 2732, 0), + new WorldPoint(1587, 2732, 0), + new WorldPoint(1595, 2728, 0), + new WorldPoint(1596, 2727, 0), + new WorldPoint(1597, 2727, 0), + new WorldPoint(1599, 2723, 0), // STOP POINT + new WorldPoint(1599, 2712, 0), + new WorldPoint(1595, 2708, 0), + new WorldPoint(1585, 2703, 0), + new WorldPoint(1584, 2702, 0), + new WorldPoint(1581, 2701, 0), + new WorldPoint(1552, 2701, 0), + new WorldPoint(1550, 2702, 0), + new WorldPoint(1540, 2712, 0), + new WorldPoint(1538, 2713, 0), + new WorldPoint(1536, 2713, 0), + new WorldPoint(1530, 2710, 0), + new WorldPoint(1529, 2709, 0), + new WorldPoint(1526, 2708, 0), + new WorldPoint(1523, 2705, 0), + new WorldPoint(1520, 2704, 0), + new WorldPoint(1519, 2703, 0), + new WorldPoint(1499, 2703, 0), + new WorldPoint(1493, 2706, 0), + new WorldPoint(1488, 2711, 0), + new WorldPoint(1486, 2711, 0), + new WorldPoint(1484, 2710, 0), + new WorldPoint(1484, 2709, 0), + new WorldPoint(1483, 2707, 0), + new WorldPoint(1483, 2704, 0), + new WorldPoint(1484, 2704, 0), // STOP POINT + new WorldPoint(1484, 2702, 0), + new WorldPoint(1485, 2700, 0), + new WorldPoint(1485, 2696, 0), + new WorldPoint(1486, 2694, 0), + new WorldPoint(1487, 2693, 0), + new WorldPoint(1489, 2692, 0), + new WorldPoint(1520, 2692, 0), + new WorldPoint(1539, 2692, 0), + new WorldPoint(1559, 2702, 0), + new WorldPoint(1562, 2705, 0), + new WorldPoint(1564, 2706, 0), + new WorldPoint(1565, 2707, 0), + new WorldPoint(1571, 2707, 0), + new WorldPoint(1575, 2705, 0), + new WorldPoint(1594, 2686, 0), // STOP POINT + new WorldPoint(1599, 2676, 0), + new WorldPoint(1599, 2657, 0), + new WorldPoint(1587, 2633, 0), + new WorldPoint(1587, 2629, 0), // STOP POINT + new WorldPoint(1586, 2627, 0), + new WorldPoint(1585, 2626, 0), + new WorldPoint(1567, 2617, 0), + new WorldPoint(1537, 2617, 0), + new WorldPoint(1523, 2617, 0), + new WorldPoint(1519, 2619, 0), // STOP POINT + new WorldPoint(1518, 2619, 0), + new WorldPoint(1517, 2620, 0), + new WorldPoint(1495, 2631, 0), + new WorldPoint(1485, 2631, 0), + new WorldPoint(1465, 2641, 0), + new WorldPoint(1464, 2641, 0), + new WorldPoint(1462, 2643, 0), + new WorldPoint(1460, 2647, 0), + new WorldPoint(1459, 2648, 0), + new WorldPoint(1458, 2651, 0), + new WorldPoint(1457, 2653, 0), + new WorldPoint(1457, 2656, 0), + new WorldPoint(1454, 2662, 0), + new WorldPoint(1453, 2663, 0), + new WorldPoint(1451, 2664, 0), + new WorldPoint(1447, 2664, 0), + new WorldPoint(1443, 2662, 0), // STOP POINT + new WorldPoint(1435, 2658, 0), + new WorldPoint(1430, 2653, 0), + new WorldPoint(1428, 2652, 0), + new WorldPoint(1413, 2652, 0), + new WorldPoint(1410, 2653, 0), + new WorldPoint(1402, 2657, 0), + new WorldPoint(1400, 2657, 0), + new WorldPoint(1399, 2658, 0), + new WorldPoint(1395, 2660, 0), + new WorldPoint(1390, 2665, 0), + new WorldPoint(1387, 2671, 0), + new WorldPoint(1387, 2674, 0), // STOP POINT + new WorldPoint(1387, 2692, 0), + new WorldPoint(1388, 2694, 0), + new WorldPoint(1389, 2695, 0), + new WorldPoint(1393, 2697, 0), + new WorldPoint(1394, 2698, 0), + new WorldPoint(1397, 2699, 0), + new WorldPoint(1399, 2700, 0), + new WorldPoint(1406, 2700, 0), + new WorldPoint(1408, 2701, 0), + new WorldPoint(1409, 2702, 0), + new WorldPoint(1410, 2704, 0), + new WorldPoint(1410, 2718, 0), + new WorldPoint(1409, 2720, 0), // STOP POINT + new WorldPoint(1408, 2722, 0), + new WorldPoint(1408, 2752, 0), + new WorldPoint(1409, 2754, 0), + new WorldPoint(1421, 2766, 0), + new WorldPoint(1425, 2768, 0), + new WorldPoint(1429, 2768, 0), // STOP POINT + new WorldPoint(1459, 2768, 0), + new WorldPoint(1471, 2768, 0), + new WorldPoint(1473, 2769, 0), + new WorldPoint(1474, 2770, 0), + new WorldPoint(1476, 2771, 0), + new WorldPoint(1477, 2771, 0), + new WorldPoint(1491, 2778, 0), + new WorldPoint(1503, 2778, 0), + new WorldPoint(1505, 2777, 0), + new WorldPoint(1506, 2776, 0), + new WorldPoint(1507, 2776, 0), + new WorldPoint(1508, 2774, 0), + new WorldPoint(1509, 2774, 0), + new WorldPoint(1517, 2766, 0), + new WorldPoint(1520, 2760, 0), + new WorldPoint(1521, 2759, 0), + new WorldPoint(1521, 2758, 0), + }; + + public static final WorldPoint[] GIANT_KRILL_SUNSET_BAY = { + new WorldPoint(1576, 2905, 0), // STOP POINT + new WorldPoint(1576, 2904, 0), + new WorldPoint(1577, 2903, 0), + new WorldPoint(1578, 2901, 0), + new WorldPoint(1593, 2886, 0), + new WorldPoint(1594, 2884, 0), + new WorldPoint(1594, 2882, 0), + new WorldPoint(1593, 2880, 0), + new WorldPoint(1593, 2878, 0), + new WorldPoint(1590, 2872, 0), // STOP POINT + new WorldPoint(1589, 2872, 0), + new WorldPoint(1589, 2871, 0), + new WorldPoint(1587, 2870, 0), + new WorldPoint(1565, 2870, 0), + new WorldPoint(1553, 2876, 0), + new WorldPoint(1552, 2876, 0), + new WorldPoint(1534, 2885, 0), + new WorldPoint(1533, 2885, 0), + new WorldPoint(1523, 2895, 0), + new WorldPoint(1523, 2896, 0), // STOP POINT + new WorldPoint(1522, 2896, 0), + new WorldPoint(1521, 2897, 0), + new WorldPoint(1519, 2898, 0), + new WorldPoint(1518, 2899, 0), + new WorldPoint(1514, 2899, 0), + new WorldPoint(1512, 2898, 0), + new WorldPoint(1511, 2897, 0), + new WorldPoint(1509, 2896, 0), + new WorldPoint(1508, 2895, 0), + new WorldPoint(1504, 2887, 0), + new WorldPoint(1504, 2884, 0), + new WorldPoint(1503, 2882, 0), + new WorldPoint(1502, 2881, 0), + new WorldPoint(1500, 2880, 0), + new WorldPoint(1496, 2880, 0), + new WorldPoint(1495, 2881, 0), + new WorldPoint(1494, 2883, 0), + new WorldPoint(1494, 2888, 0), // STOP POINT + new WorldPoint(1493, 2889, 0), + new WorldPoint(1490, 2895, 0), + new WorldPoint(1488, 2897, 0), + new WorldPoint(1487, 2899, 0), + new WorldPoint(1487, 2901, 0), + new WorldPoint(1489, 2905, 0), + new WorldPoint(1495, 2911, 0), + new WorldPoint(1497, 2915, 0), + new WorldPoint(1497, 2921, 0), // STOP POINT + new WorldPoint(1497, 2943, 0), + new WorldPoint(1498, 2945, 0), + new WorldPoint(1499, 2946, 0), + new WorldPoint(1499, 2947, 0), + new WorldPoint(1503, 2949, 0), + new WorldPoint(1504, 2949, 0), // STOP POINT + new WorldPoint(1505, 2949, 0), + new WorldPoint(1507, 2948, 0), + new WorldPoint(1510, 2945, 0), + new WorldPoint(1519, 2927, 0), + new WorldPoint(1519, 2926, 0), + new WorldPoint(1520, 2924, 0), + new WorldPoint(1522, 2922, 0), + new WorldPoint(1525, 2922, 0), + new WorldPoint(1527, 2923, 0), + new WorldPoint(1528, 2923, 0), + new WorldPoint(1530, 2924, 0), + new WorldPoint(1543, 2937, 0), + new WorldPoint(1545, 2938, 0), + new WorldPoint(1563, 2938, 0), + new WorldPoint(1567, 2936, 0), + new WorldPoint(1569, 2936, 0), // STOP POINT + new WorldPoint(1569, 2935, 0), + new WorldPoint(1571, 2935, 0), + new WorldPoint(1575, 2933, 0), + new WorldPoint(1577, 2931, 0), + new WorldPoint(1578, 2929, 0), + new WorldPoint(1578, 2927, 0), + new WorldPoint(1576, 2923, 0), + new WorldPoint(1576, 2922, 0), + new WorldPoint(1575, 2921, 0), + new WorldPoint(1574, 2919, 0), + new WorldPoint(1574, 2909, 0), + new WorldPoint(1576, 2905, 0), + }; + + public static final WorldPoint[] HADDOCK_ANGLERFISHS_LIGHT = { + new WorldPoint(2772, 2411, 0), // STOP POINT + new WorldPoint(2756, 2411, 0), + new WorldPoint(2755, 2410, 0), + new WorldPoint(2754, 2410, 0), + new WorldPoint(2742, 2398, 0), + new WorldPoint(2741, 2396, 0), + new WorldPoint(2741, 2378, 0), // STOP POINT + new WorldPoint(2745, 2370, 0), + new WorldPoint(2746, 2369, 0), + new WorldPoint(2746, 2367, 0), + new WorldPoint(2748, 2365, 0), + new WorldPoint(2752, 2363, 0), + new WorldPoint(2757, 2363, 0), + new WorldPoint(2759, 2362, 0), + new WorldPoint(2760, 2361, 0), + new WorldPoint(2761, 2359, 0), + new WorldPoint(2761, 2349, 0), + new WorldPoint(2760, 2348, 0), + new WorldPoint(2760, 2347, 0), + new WorldPoint(2761, 2345, 0), + new WorldPoint(2765, 2341, 0), + new WorldPoint(2767, 2340, 0), + new WorldPoint(2775, 2340, 0), // STOP POINT + new WorldPoint(2781, 2340, 0), + new WorldPoint(2785, 2338, 0), + new WorldPoint(2787, 2336, 0), + new WorldPoint(2790, 2330, 0), + new WorldPoint(2792, 2328, 0), + new WorldPoint(2792, 2323, 0), + new WorldPoint(2793, 2321, 0), + new WorldPoint(2794, 2320, 0), + new WorldPoint(2798, 2318, 0), + new WorldPoint(2799, 2317, 0), + new WorldPoint(2801, 2316, 0), + new WorldPoint(2804, 2313, 0), + new WorldPoint(2804, 2312, 0), + new WorldPoint(2805, 2310, 0), + new WorldPoint(2804, 2308, 0), + new WorldPoint(2802, 2306, 0), + new WorldPoint(2800, 2305, 0), + new WorldPoint(2788, 2305, 0), + new WorldPoint(2770, 2314, 0), // STOP POINT + new WorldPoint(2748, 2314, 0), + new WorldPoint(2746, 2313, 0), + new WorldPoint(2745, 2312, 0), + new WorldPoint(2743, 2313, 0), + new WorldPoint(2734, 2322, 0), + new WorldPoint(2730, 2324, 0), + new WorldPoint(2702, 2324, 0), // STOP POINT + new WorldPoint(2684, 2333, 0), + new WorldPoint(2682, 2337, 0), + new WorldPoint(2682, 2348, 0), + new WorldPoint(2684, 2352, 0), + new WorldPoint(2688, 2356, 0), + new WorldPoint(2700, 2362, 0), + new WorldPoint(2702, 2362, 0), + new WorldPoint(2704, 2363, 0), + new WorldPoint(2705, 2364, 0), + new WorldPoint(2713, 2380, 0), + new WorldPoint(2713, 2411, 0), + new WorldPoint(2713, 2420, 0), // STOP POINT + new WorldPoint(2714, 2421, 0), + new WorldPoint(2719, 2431, 0), + new WorldPoint(2719, 2432, 0), + new WorldPoint(2720, 2434, 0), + new WorldPoint(2726, 2440, 0), + new WorldPoint(2729, 2441, 0), + new WorldPoint(2730, 2442, 0), + new WorldPoint(2732, 2443, 0), + new WorldPoint(2760, 2443, 0), + new WorldPoint(2761, 2442, 0), + new WorldPoint(2763, 2441, 0), + new WorldPoint(2772, 2432, 0), + new WorldPoint(2775, 2431, 0), + new WorldPoint(2783, 2431, 0), // STOP POINT + new WorldPoint(2791, 2427, 0), + new WorldPoint(2792, 2426, 0), + new WorldPoint(2798, 2423, 0), + new WorldPoint(2808, 2413, 0), + new WorldPoint(2810, 2412, 0), + new WorldPoint(2810, 2411, 0), + new WorldPoint(2812, 2409, 0), + new WorldPoint(2823, 2387, 0), + new WorldPoint(2823, 2384, 0), + new WorldPoint(2817, 2378, 0), // STOP POINT + new WorldPoint(2817, 2377, 0), + new WorldPoint(2815, 2377, 0), + new WorldPoint(2813, 2376, 0), + new WorldPoint(2803, 2376, 0), + new WorldPoint(2802, 2377, 0), + new WorldPoint(2802, 2383, 0), + new WorldPoint(2791, 2405, 0), + new WorldPoint(2791, 2406, 0), + new WorldPoint(2790, 2407, 0), + new WorldPoint(2782, 2411, 0), + new WorldPoint(2772, 2411, 0), + }; + + public static final WorldPoint[] HADDOCK_THE_ONYX_CREST = { + new WorldPoint(3096, 2214, 0), // STOP POINT + new WorldPoint(3092, 2214, 0), + new WorldPoint(3076, 2206, 0), + new WorldPoint(3075, 2206, 0), + new WorldPoint(3059, 2198, 0), // STOP POINT + new WorldPoint(3037, 2176, 0), + new WorldPoint(3033, 2172, 0), + new WorldPoint(3033, 2170, 0), + new WorldPoint(3031, 2168, 0), + new WorldPoint(3029, 2167, 0), + new WorldPoint(3027, 2167, 0), + new WorldPoint(3009, 2176, 0), + new WorldPoint(3003, 2182, 0), + new WorldPoint(3002, 2184, 0), + new WorldPoint(3002, 2200, 0), + new WorldPoint(3004, 2204, 0), // STOP POINT + new WorldPoint(3004, 2206, 0), + new WorldPoint(3003, 2208, 0), + new WorldPoint(3002, 2209, 0), + new WorldPoint(3000, 2210, 0), + new WorldPoint(2975, 2210, 0), + new WorldPoint(2974, 2211, 0), + new WorldPoint(2968, 2214, 0), + new WorldPoint(2965, 2215, 0), + new WorldPoint(2964, 2216, 0), + new WorldPoint(2962, 2216, 0), + new WorldPoint(2959, 2219, 0), + new WorldPoint(2959, 2221, 0), + new WorldPoint(2960, 2224, 0), + new WorldPoint(2961, 2225, 0), + new WorldPoint(2963, 2226, 0), + new WorldPoint(2965, 2226, 0), + new WorldPoint(2967, 2225, 0), + new WorldPoint(2969, 2225, 0), + new WorldPoint(2975, 2222, 0), // STOP POINT + new WorldPoint(3005, 2222, 0), + new WorldPoint(3007, 2224, 0), + new WorldPoint(3010, 2230, 0), + new WorldPoint(3010, 2235, 0), + new WorldPoint(3011, 2237, 0), + new WorldPoint(3016, 2242, 0), + new WorldPoint(3026, 2247, 0), + new WorldPoint(3027, 2247, 0), + new WorldPoint(3028, 2248, 0), + new WorldPoint(3042, 2255, 0), + new WorldPoint(3049, 2262, 0), + new WorldPoint(3051, 2266, 0), + new WorldPoint(3048, 2272, 0), + new WorldPoint(3046, 2275, 0), + new WorldPoint(3046, 2276, 0), + new WorldPoint(3039, 2290, 0), + new WorldPoint(3024, 2305, 0), + new WorldPoint(3022, 2306, 0), // STOP POINT + new WorldPoint(3021, 2306, 0), + new WorldPoint(3020, 2307, 0), + new WorldPoint(3018, 2308, 0), + new WorldPoint(3008, 2308, 0), + new WorldPoint(3006, 2309, 0), + new WorldPoint(2999, 2316, 0), + new WorldPoint(2997, 2320, 0), + new WorldPoint(2991, 2326, 0), + new WorldPoint(2989, 2327, 0), + new WorldPoint(2988, 2328, 0), + new WorldPoint(2986, 2328, 0), + new WorldPoint(2984, 2327, 0), + new WorldPoint(2967, 2310, 0), + new WorldPoint(2965, 2309, 0), + new WorldPoint(2959, 2309, 0), + new WorldPoint(2951, 2313, 0), // STOP POINT + new WorldPoint(2947, 2315, 0), + new WorldPoint(2942, 2315, 0), + new WorldPoint(2939, 2318, 0), + new WorldPoint(2939, 2322, 0), + new WorldPoint(2940, 2325, 0), + new WorldPoint(2943, 2331, 0), + new WorldPoint(2944, 2332, 0), + new WorldPoint(2944, 2351, 0), + new WorldPoint(2946, 2355, 0), + new WorldPoint(2946, 2357, 0), + new WorldPoint(2947, 2358, 0), + new WorldPoint(2948, 2360, 0), + new WorldPoint(2949, 2361, 0), + new WorldPoint(2957, 2365, 0), + new WorldPoint(2974, 2365, 0), // STOP POINT + new WorldPoint(2991, 2365, 0), + new WorldPoint(2993, 2364, 0), + new WorldPoint(2994, 2363, 0), + new WorldPoint(2999, 2353, 0), + new WorldPoint(2999, 2348, 0), + new WorldPoint(3000, 2346, 0), + new WorldPoint(3001, 2343, 0), + new WorldPoint(3002, 2342, 0), + new WorldPoint(3002, 2341, 0), + new WorldPoint(3003, 2340, 0), + new WorldPoint(3005, 2339, 0), + new WorldPoint(3011, 2339, 0), + new WorldPoint(3013, 2340, 0), + new WorldPoint(3014, 2341, 0), + new WorldPoint(3015, 2341, 0), + new WorldPoint(3017, 2342, 0), + new WorldPoint(3018, 2343, 0), + new WorldPoint(3023, 2353, 0), + new WorldPoint(3024, 2354, 0), + new WorldPoint(3025, 2356, 0), + new WorldPoint(3026, 2357, 0), + new WorldPoint(3028, 2358, 0), + new WorldPoint(3030, 2358, 0), + new WorldPoint(3038, 2354, 0), + new WorldPoint(3041, 2351, 0), // STOP POINT + new WorldPoint(3047, 2345, 0), + new WorldPoint(3053, 2342, 0), + new WorldPoint(3054, 2342, 0), + new WorldPoint(3056, 2340, 0), + new WorldPoint(3058, 2336, 0), + new WorldPoint(3058, 2311, 0), + new WorldPoint(3062, 2303, 0), + new WorldPoint(3065, 2300, 0), + new WorldPoint(3068, 2300, 0), + new WorldPoint(3069, 2301, 0), + new WorldPoint(3071, 2302, 0), + new WorldPoint(3073, 2304, 0), + new WorldPoint(3074, 2306, 0), + new WorldPoint(3074, 2307, 0), + new WorldPoint(3075, 2308, 0), + new WorldPoint(3077, 2309, 0), + new WorldPoint(3081, 2309, 0), + new WorldPoint(3085, 2307, 0), + new WorldPoint(3087, 2307, 0), + new WorldPoint(3093, 2304, 0), + new WorldPoint(3097, 2304, 0), // STOP POINT + new WorldPoint(3109, 2298, 0), + new WorldPoint(3112, 2295, 0), + new WorldPoint(3113, 2292, 0), + new WorldPoint(3114, 2291, 0), + new WorldPoint(3114, 2285, 0), + new WorldPoint(3113, 2283, 0), + new WorldPoint(3112, 2282, 0), + new WorldPoint(3100, 2276, 0), + new WorldPoint(3099, 2276, 0), + new WorldPoint(3094, 2271, 0), + new WorldPoint(3089, 2261, 0), + new WorldPoint(3087, 2259, 0), + new WorldPoint(3086, 2256, 0), // STOP POINT + new WorldPoint(3086, 2245, 0), + new WorldPoint(3087, 2243, 0), + new WorldPoint(3099, 2231, 0), + new WorldPoint(3107, 2227, 0), + new WorldPoint(3109, 2225, 0), + new WorldPoint(3110, 2223, 0), + new WorldPoint(3110, 2221, 0), + new WorldPoint(3109, 2219, 0), + new WorldPoint(3108, 2218, 0), + new WorldPoint(3100, 2214, 0), + new WorldPoint(3096, 2214, 0), + }; + + public static final WorldPoint[] YELLOWFIN_SEA_OF_SOULS = { + new WorldPoint(2224, 2738, 0), // STOP POINT + new WorldPoint(2234, 2738, 0), + new WorldPoint(2236, 2737, 0), + new WorldPoint(2237, 2736, 0), + new WorldPoint(2239, 2733, 0), + new WorldPoint(2239, 2730, 0), + new WorldPoint(2238, 2728, 0), + new WorldPoint(2237, 2725, 0), + new WorldPoint(2236, 2723, 0), + new WorldPoint(2234, 2720, 0), + new WorldPoint(2232, 2716, 0), + new WorldPoint(2232, 2715, 0), + new WorldPoint(2231, 2714, 0), + new WorldPoint(2229, 2713, 0), + new WorldPoint(2199, 2713, 0), + new WorldPoint(2196, 2713, 0), + new WorldPoint(2194, 2712, 0), + new WorldPoint(2193, 2710, 0), + new WorldPoint(2191, 2707, 0), // STOP POINT + new WorldPoint(2191, 2702, 0), + new WorldPoint(2192, 2700, 0), + new WorldPoint(2193, 2697, 0), + new WorldPoint(2196, 2691, 0), + new WorldPoint(2196, 2690, 0), + new WorldPoint(2197, 2689, 0), + new WorldPoint(2198, 2687, 0), + new WorldPoint(2200, 2686, 0), + new WorldPoint(2203, 2684, 0), + new WorldPoint(2233, 2684, 0), + new WorldPoint(2248, 2684, 0), + new WorldPoint(2251, 2683, 0), + new WorldPoint(2253, 2681, 0), + new WorldPoint(2256, 2680, 0), + new WorldPoint(2258, 2679, 0), + new WorldPoint(2261, 2678, 0), + new WorldPoint(2263, 2676, 0), + new WorldPoint(2266, 2675, 0), + new WorldPoint(2268, 2674, 0), + new WorldPoint(2270, 2674, 0), // STOP POINT + new WorldPoint(2283, 2674, 0), + new WorldPoint(2285, 2675, 0), + new WorldPoint(2288, 2676, 0), + new WorldPoint(2290, 2677, 0), + new WorldPoint(2303, 2690, 0), // STOP POINT + new WorldPoint(2305, 2691, 0), + new WorldPoint(2308, 2692, 0), + new WorldPoint(2310, 2693, 0), + new WorldPoint(2340, 2693, 0), + new WorldPoint(2348, 2693, 0), + new WorldPoint(2351, 2692, 0), + new WorldPoint(2352, 2691, 0), + new WorldPoint(2354, 2688, 0), + new WorldPoint(2354, 2658, 0), + new WorldPoint(2354, 2651, 0), // STOP POINT + new WorldPoint(2354, 2621, 0), + new WorldPoint(2354, 2611, 0), + new WorldPoint(2353, 2609, 0), + new WorldPoint(2352, 2608, 0), + new WorldPoint(2352, 2607, 0), + new WorldPoint(2350, 2606, 0), + new WorldPoint(2347, 2605, 0), + new WorldPoint(2340, 2605, 0), + new WorldPoint(2339, 2606, 0), + new WorldPoint(2337, 2607, 0), + new WorldPoint(2319, 2625, 0), + new WorldPoint(2315, 2627, 0), + new WorldPoint(2312, 2628, 0), + new WorldPoint(2311, 2629, 0), + new WorldPoint(2301, 2629, 0), + new WorldPoint(2298, 2628, 0), + new WorldPoint(2297, 2627, 0), + new WorldPoint(2297, 2626, 0), + new WorldPoint(2296, 2624, 0), + new WorldPoint(2296, 2622, 0), + new WorldPoint(2297, 2620, 0), + new WorldPoint(2298, 2617, 0), + new WorldPoint(2300, 2613, 0), + new WorldPoint(2301, 2612, 0), + new WorldPoint(2303, 2608, 0), + new WorldPoint(2303, 2602, 0), + new WorldPoint(2301, 2598, 0), + new WorldPoint(2298, 2597, 0), + new WorldPoint(2296, 2596, 0), + new WorldPoint(2293, 2595, 0), + new WorldPoint(2287, 2595, 0), // STOP POINT + new WorldPoint(2277, 2595, 0), + new WorldPoint(2273, 2597, 0), + new WorldPoint(2270, 2598, 0), + new WorldPoint(2266, 2600, 0), + new WorldPoint(2256, 2600, 0), + new WorldPoint(2252, 2598, 0), + new WorldPoint(2249, 2597, 0), + new WorldPoint(2247, 2596, 0), + new WorldPoint(2246, 2595, 0), + new WorldPoint(2216, 2595, 0), + new WorldPoint(2188, 2595, 0), + new WorldPoint(2186, 2596, 0), + new WorldPoint(2185, 2597, 0), + new WorldPoint(2183, 2600, 0), + new WorldPoint(2183, 2602, 0), + new WorldPoint(2184, 2605, 0), + new WorldPoint(2185, 2606, 0), + new WorldPoint(2185, 2608, 0), + new WorldPoint(2186, 2609, 0), + new WorldPoint(2189, 2610, 0), + new WorldPoint(2191, 2611, 0), + new WorldPoint(2221, 2611, 0), + new WorldPoint(2224, 2611, 0), // STOP POINT + new WorldPoint(2236, 2611, 0), + new WorldPoint(2237, 2612, 0), + new WorldPoint(2239, 2615, 0), + new WorldPoint(2239, 2618, 0), + new WorldPoint(2238, 2620, 0), + new WorldPoint(2237, 2623, 0), + new WorldPoint(2236, 2625, 0), + new WorldPoint(2234, 2628, 0), + new WorldPoint(2232, 2632, 0), + new WorldPoint(2232, 2633, 0), + new WorldPoint(2231, 2634, 0), + new WorldPoint(2229, 2635, 0), + new WorldPoint(2199, 2635, 0), + new WorldPoint(2196, 2635, 0), + new WorldPoint(2194, 2636, 0), + new WorldPoint(2193, 2638, 0), + new WorldPoint(2191, 2641, 0), // STOP POINT + new WorldPoint(2191, 2646, 0), + new WorldPoint(2192, 2648, 0), + new WorldPoint(2193, 2651, 0), + new WorldPoint(2198, 2661, 0), + new WorldPoint(2199, 2662, 0), + new WorldPoint(2203, 2664, 0), + new WorldPoint(2234, 2664, 0), + new WorldPoint(2249, 2664, 0), + new WorldPoint(2251, 2666, 0), + new WorldPoint(2254, 2667, 0), + new WorldPoint(2256, 2668, 0), + new WorldPoint(2259, 2669, 0), + new WorldPoint(2261, 2671, 0), + new WorldPoint(2267, 2673, 0), + new WorldPoint(2268, 2674, 0), + new WorldPoint(2269, 2674, 0), // STOP POINT + new WorldPoint(2284, 2674, 0), + new WorldPoint(2285, 2673, 0), + new WorldPoint(2287, 2672, 0), + new WorldPoint(2290, 2671, 0), + new WorldPoint(2303, 2658, 0), // STOP POINT + new WorldPoint(2307, 2656, 0), + new WorldPoint(2310, 2655, 0), + new WorldPoint(2340, 2655, 0), + new WorldPoint(2348, 2655, 0), + new WorldPoint(2349, 2656, 0), + new WorldPoint(2351, 2657, 0), + new WorldPoint(2353, 2661, 0), + new WorldPoint(2354, 2664, 0), + new WorldPoint(2354, 2694, 0), + new WorldPoint(2354, 2696, 0), // STOP POINT + new WorldPoint(2354, 2726, 0), + new WorldPoint(2354, 2737, 0), + new WorldPoint(2353, 2739, 0), + new WorldPoint(2351, 2741, 0), + new WorldPoint(2347, 2743, 0), + new WorldPoint(2341, 2743, 0), + new WorldPoint(2337, 2741, 0), + new WorldPoint(2320, 2724, 0), + new WorldPoint(2316, 2722, 0), + new WorldPoint(2313, 2720, 0), + new WorldPoint(2312, 2719, 0), + new WorldPoint(2301, 2719, 0), + new WorldPoint(2299, 2720, 0), + new WorldPoint(2297, 2722, 0), + new WorldPoint(2296, 2724, 0), + new WorldPoint(2296, 2726, 0), + new WorldPoint(2297, 2729, 0), + new WorldPoint(2298, 2731, 0), + new WorldPoint(2299, 2734, 0), + new WorldPoint(2301, 2736, 0), + new WorldPoint(2303, 2740, 0), + new WorldPoint(2303, 2747, 0), + new WorldPoint(2302, 2749, 0), + new WorldPoint(2301, 2750, 0), + new WorldPoint(2299, 2751, 0), + new WorldPoint(2296, 2752, 0), + new WorldPoint(2294, 2753, 0), + new WorldPoint(2287, 2753, 0), // STOP POINT + new WorldPoint(2277, 2753, 0), + new WorldPoint(2273, 2751, 0), + new WorldPoint(2270, 2750, 0), + new WorldPoint(2266, 2748, 0), + new WorldPoint(2256, 2748, 0), + new WorldPoint(2254, 2749, 0), + new WorldPoint(2251, 2750, 0), + new WorldPoint(2247, 2752, 0), + new WorldPoint(2246, 2753, 0), + new WorldPoint(2214, 2753, 0), + new WorldPoint(2188, 2753, 0), + new WorldPoint(2186, 2752, 0), + new WorldPoint(2185, 2751, 0), + new WorldPoint(2183, 2748, 0), + new WorldPoint(2183, 2746, 0), + new WorldPoint(2184, 2744, 0), + new WorldPoint(2185, 2741, 0), + new WorldPoint(2185, 2740, 0), + new WorldPoint(2186, 2739, 0), + new WorldPoint(2189, 2738, 0), + new WorldPoint(2219, 2738, 0), + new WorldPoint(2224, 2738, 0), + }; + + public static final WorldPoint[] YELLOWFIN_DEEPFIN_POINT = { + new WorldPoint(1900, 2721, 0), // STOP POINT + new WorldPoint(1900, 2741, 0), + new WorldPoint(1901, 2742, 0), + new WorldPoint(1901, 2743, 0), + new WorldPoint(1905, 2745, 0), + new WorldPoint(1910, 2750, 0), + new WorldPoint(1911, 2752, 0), + new WorldPoint(1911, 2755, 0), + new WorldPoint(1909, 2759, 0), + new WorldPoint(1908, 2762, 0), + new WorldPoint(1906, 2765, 0), + new WorldPoint(1905, 2767, 0), + new WorldPoint(1904, 2770, 0), + new WorldPoint(1900, 2774, 0), + new WorldPoint(1898, 2775, 0), + new WorldPoint(1895, 2777, 0), + new WorldPoint(1893, 2778, 0), + new WorldPoint(1890, 2779, 0), + new WorldPoint(1888, 2780, 0), + new WorldPoint(1885, 2782, 0), + new WorldPoint(1870, 2782, 0), // STOP POINT + new WorldPoint(1868, 2783, 0), + new WorldPoint(1867, 2784, 0), + new WorldPoint(1865, 2787, 0), + new WorldPoint(1865, 2789, 0), + new WorldPoint(1866, 2792, 0), + new WorldPoint(1868, 2794, 0), + new WorldPoint(1869, 2797, 0), + new WorldPoint(1872, 2803, 0), + new WorldPoint(1880, 2811, 0), + new WorldPoint(1882, 2812, 0), + new WorldPoint(1895, 2812, 0), + new WorldPoint(1896, 2813, 0), + new WorldPoint(1896, 2814, 0), + new WorldPoint(1897, 2815, 0), + new WorldPoint(1899, 2818, 0), + new WorldPoint(1900, 2820, 0), + new WorldPoint(1901, 2823, 0), + new WorldPoint(1902, 2825, 0), + new WorldPoint(1902, 2829, 0), + new WorldPoint(1898, 2837, 0), + new WorldPoint(1896, 2839, 0), + new WorldPoint(1892, 2841, 0), // STOP POINT + new WorldPoint(1891, 2842, 0), + new WorldPoint(1890, 2844, 0), + new WorldPoint(1889, 2847, 0), + new WorldPoint(1888, 2849, 0), + new WorldPoint(1888, 2855, 0), + new WorldPoint(1886, 2859, 0), + new WorldPoint(1885, 2860, 0), + new WorldPoint(1883, 2861, 0), + new WorldPoint(1882, 2862, 0), + new WorldPoint(1881, 2864, 0), + new WorldPoint(1880, 2865, 0), + new WorldPoint(1878, 2866, 0), + new WorldPoint(1875, 2867, 0), + new WorldPoint(1873, 2869, 0), + new WorldPoint(1871, 2870, 0), + new WorldPoint(1861, 2870, 0), + new WorldPoint(1859, 2869, 0), + new WorldPoint(1858, 2868, 0), + new WorldPoint(1857, 2865, 0), + new WorldPoint(1855, 2863, 0), + new WorldPoint(1854, 2861, 0), + new WorldPoint(1854, 2854, 0), + new WorldPoint(1853, 2852, 0), + new WorldPoint(1852, 2852, 0), + new WorldPoint(1850, 2851, 0), + new WorldPoint(1846, 2851, 0), + new WorldPoint(1845, 2852, 0), + new WorldPoint(1843, 2852, 0), + new WorldPoint(1830, 2865, 0), + new WorldPoint(1828, 2866, 0), + new WorldPoint(1825, 2867, 0), + new WorldPoint(1821, 2867, 0), // STOP POINT + new WorldPoint(1816, 2867, 0), + new WorldPoint(1812, 2863, 0), + new WorldPoint(1811, 2861, 0), + new WorldPoint(1810, 2858, 0), + new WorldPoint(1809, 2856, 0), + new WorldPoint(1807, 2853, 0), + new WorldPoint(1807, 2847, 0), + new WorldPoint(1808, 2845, 0), + new WorldPoint(1809, 2844, 0), + new WorldPoint(1810, 2842, 0), + new WorldPoint(1812, 2841, 0), + new WorldPoint(1815, 2838, 0), + new WorldPoint(1815, 2836, 0), + new WorldPoint(1816, 2834, 0), + new WorldPoint(1817, 2831, 0), + new WorldPoint(1818, 2829, 0), + new WorldPoint(1819, 2826, 0), + new WorldPoint(1819, 2820, 0), + new WorldPoint(1818, 2818, 0), + new WorldPoint(1812, 2812, 0), + new WorldPoint(1810, 2811, 0), + new WorldPoint(1808, 2809, 0), + new WorldPoint(1805, 2808, 0), + new WorldPoint(1803, 2807, 0), + new WorldPoint(1800, 2806, 0), + new WorldPoint(1796, 2804, 0), // STOP POINT + new WorldPoint(1795, 2804, 0), + new WorldPoint(1793, 2803, 0), + new WorldPoint(1792, 2802, 0), + new WorldPoint(1791, 2799, 0), + new WorldPoint(1791, 2769, 0), + new WorldPoint(1791, 2762, 0), + new WorldPoint(1793, 2759, 0), + new WorldPoint(1794, 2757, 0), + new WorldPoint(1795, 2754, 0), + new WorldPoint(1797, 2750, 0), + new WorldPoint(1797, 2749, 0), + new WorldPoint(1800, 2743, 0), + new WorldPoint(1806, 2737, 0), + new WorldPoint(1807, 2737, 0), + new WorldPoint(1809, 2735, 0), + new WorldPoint(1811, 2734, 0), + new WorldPoint(1823, 2734, 0), // STOP POINT + new WorldPoint(1825, 2735, 0), + new WorldPoint(1826, 2736, 0), + new WorldPoint(1827, 2739, 0), + new WorldPoint(1828, 2741, 0), + new WorldPoint(1830, 2744, 0), + new WorldPoint(1831, 2746, 0), + new WorldPoint(1832, 2749, 0), + new WorldPoint(1833, 2751, 0), + new WorldPoint(1835, 2754, 0), + new WorldPoint(1836, 2756, 0), + new WorldPoint(1837, 2759, 0), + new WorldPoint(1838, 2761, 0), + new WorldPoint(1840, 2764, 0), + new WorldPoint(1840, 2770, 0), + new WorldPoint(1841, 2772, 0), + new WorldPoint(1849, 2780, 0), + new WorldPoint(1851, 2781, 0), + new WorldPoint(1854, 2782, 0), + new WorldPoint(1865, 2782, 0), // STOP POINT + new WorldPoint(1867, 2783, 0), + new WorldPoint(1868, 2784, 0), + new WorldPoint(1869, 2787, 0), + new WorldPoint(1870, 2789, 0), + new WorldPoint(1872, 2792, 0), + new WorldPoint(1872, 2803, 0), + new WorldPoint(1873, 2805, 0), + new WorldPoint(1877, 2809, 0), + new WorldPoint(1878, 2809, 0), + new WorldPoint(1879, 2811, 0), + new WorldPoint(1880, 2811, 0), + new WorldPoint(1882, 2812, 0), + new WorldPoint(1895, 2812, 0), + new WorldPoint(1897, 2814, 0), + new WorldPoint(1900, 2820, 0), + new WorldPoint(1901, 2823, 0), + new WorldPoint(1902, 2825, 0), + new WorldPoint(1902, 2829, 0), + new WorldPoint(1903, 2831, 0), + new WorldPoint(1907, 2835, 0), + new WorldPoint(1908, 2835, 0), + new WorldPoint(1911, 2837, 0), + new WorldPoint(1913, 2838, 0), + new WorldPoint(1916, 2839, 0), + new WorldPoint(1918, 2841, 0), + new WorldPoint(1920, 2842, 0), + new WorldPoint(1934, 2842, 0), // STOP POINT + new WorldPoint(1938, 2844, 0), + new WorldPoint(1944, 2850, 0), + new WorldPoint(1948, 2852, 0), + new WorldPoint(1967, 2852, 0), + new WorldPoint(1969, 2850, 0), + new WorldPoint(1970, 2848, 0), + new WorldPoint(1970, 2842, 0), + new WorldPoint(1971, 2840, 0), + new WorldPoint(1972, 2837, 0), + new WorldPoint(1975, 2834, 0), + new WorldPoint(1978, 2833, 0), + new WorldPoint(1980, 2832, 0), + new WorldPoint(1984, 2832, 0), + new WorldPoint(1986, 2833, 0), + new WorldPoint(1987, 2834, 0), + new WorldPoint(1989, 2837, 0), + new WorldPoint(1990, 2839, 0), + new WorldPoint(1991, 2842, 0), + new WorldPoint(1991, 2845, 0), + new WorldPoint(1995, 2849, 0), + new WorldPoint(2000, 2849, 0), + new WorldPoint(2004, 2847, 0), + new WorldPoint(2005, 2846, 0), + new WorldPoint(2007, 2845, 0), + new WorldPoint(2010, 2842, 0), + new WorldPoint(2011, 2839, 0), + new WorldPoint(2011, 2808, 0), + new WorldPoint(2011, 2804, 0), + new WorldPoint(2010, 2802, 0), + new WorldPoint(2009, 2801, 0), + new WorldPoint(2011, 2797, 0), + new WorldPoint(2012, 2796, 0), + new WorldPoint(2014, 2792, 0), + new WorldPoint(2014, 2787, 0), // STOP POINT + new WorldPoint(2014, 2778, 0), + new WorldPoint(2013, 2776, 0), + new WorldPoint(2013, 2775, 0), + new WorldPoint(2012, 2773, 0), + new WorldPoint(2011, 2772, 0), + new WorldPoint(2010, 2770, 0), + new WorldPoint(2010, 2753, 0), + new WorldPoint(2011, 2750, 0), + new WorldPoint(2011, 2749, 0), + new WorldPoint(2012, 2747, 0), + new WorldPoint(2010, 2743, 0), + new WorldPoint(2002, 2735, 0), + new WorldPoint(1999, 2734, 0), + new WorldPoint(1997, 2733, 0), // STOP POINT + new WorldPoint(1995, 2732, 0), + new WorldPoint(1992, 2730, 0), + new WorldPoint(1990, 2729, 0), + new WorldPoint(1987, 2728, 0), + new WorldPoint(1983, 2726, 0), + new WorldPoint(1981, 2726, 0), + new WorldPoint(1979, 2725, 0), + new WorldPoint(1971, 2717, 0), + new WorldPoint(1970, 2715, 0), + new WorldPoint(1969, 2714, 0), + new WorldPoint(1969, 2696, 0), + new WorldPoint(1970, 2695, 0), + new WorldPoint(1971, 2695, 0), + new WorldPoint(1972, 2693, 0), + new WorldPoint(1974, 2692, 0), + new WorldPoint(1978, 2688, 0), + new WorldPoint(1984, 2685, 0), + new WorldPoint(1986, 2685, 0), + new WorldPoint(1986, 2684, 0), + new WorldPoint(1987, 2683, 0), + new WorldPoint(1987, 2680, 0), + new WorldPoint(1986, 2679, 0), + new WorldPoint(1985, 2677, 0), + new WorldPoint(1985, 2676, 0), + new WorldPoint(1983, 2675, 0), + new WorldPoint(1977, 2675, 0), // STOP POINT + new WorldPoint(1947, 2675, 0), + new WorldPoint(1920, 2675, 0), + new WorldPoint(1917, 2676, 0), + new WorldPoint(1916, 2676, 0), + new WorldPoint(1915, 2677, 0), + new WorldPoint(1914, 2679, 0), + new WorldPoint(1912, 2680, 0), + new WorldPoint(1911, 2681, 0), + new WorldPoint(1910, 2683, 0), + new WorldPoint(1909, 2684, 0), + new WorldPoint(1907, 2687, 0), + new WorldPoint(1906, 2689, 0), + new WorldPoint(1906, 2709, 0), + new WorldPoint(1904, 2713, 0), + new WorldPoint(1902, 2716, 0), + new WorldPoint(1900, 2720, 0), + new WorldPoint(1900, 2721, 0), + }; +} diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalTracker.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalTracker.java new file mode 100644 index 00000000..824e78ac --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalTracker.java @@ -0,0 +1,517 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.features.util.BoatTracker; +import com.duckblade.osrs.sailing.features.util.SailingUtil; +import com.duckblade.osrs.sailing.model.Boat; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import com.duckblade.osrs.sailing.model.ShoalDepth; +import com.google.common.collect.ImmutableSet; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.GameObject; +import net.runelite.api.NPC; +import net.runelite.api.WorldEntity; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameObjectDespawned; +import net.runelite.api.events.GameObjectSpawned; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; +import net.runelite.api.events.WorldEntitySpawned; +import net.runelite.api.events.WorldViewUnloaded; +import net.runelite.client.Notifier; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.api.gameval.AnimationID; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static net.runelite.api.gameval.NpcID.SAILING_SHOAL_RIPPLES; + +/** + * Centralized tracker for shoal WorldEntity and GameObject instances. + * Provides a single source of truth for shoal state across all trawling components. + * Shoals are a WorldEntity (moving object), GameObject, and an NPC (renderable). All + * three are required for detection of movement, spawn/despawn, and shoal depth. + */ +@Slf4j +@Singleton +public class ShoalTracker implements PluginLifecycleComponent { + + // WorldEntity config ID for moving shoals + private static final int SHOAL_WORLD_ENTITY_CONFIG_ID = 4; + private static final int SHOAL_DEPTH_SHALLOW = AnimationID.DEEP_SEA_TRAWLING_SHOAL_SHALLOW; + private static final int SHOAL_DEPTH_MODERATE = AnimationID.DEEP_SEA_TRAWLING_SHOAL_MID; + private static final int SHOAL_DEPTH_DEEP = AnimationID.DEEP_SEA_TRAWLING_SHOAL_DEEP; + + private static final Set SHOAL_OBJECT_IDS = ImmutableSet.of( + TrawlingData.ShoalObjectID.MARLIN, + TrawlingData.ShoalObjectID.BLUEFIN, + TrawlingData.ShoalObjectID.VIBRANT, + TrawlingData.ShoalObjectID.HALIBUT, + TrawlingData.ShoalObjectID.GLISTENING, + TrawlingData.ShoalObjectID.YELLOWFIN, + TrawlingData.ShoalObjectID.GIANT_KRILL, + TrawlingData.ShoalObjectID.HADDOCK, + TrawlingData.ShoalObjectID.SHIMMERING + ); + + private final Client client; + private final Notifier notifier; + private final SailingConfig config; + private final BoatTracker boatTracker; + + /** + * -- GETTER -- + * Get the current shoal WorldEntity (for movement tracking) + */ + // Tracked state + @Getter + private WorldEntity currentShoalEntity = null; + private final Map shoalObjects = new HashMap<>(); + /** + * -- GETTER -- + * Get the current shoal location + */ + @Getter + private WorldPoint currentLocation = null; + /** + * -- GETTER -- + * Get the shoal duration for the current location + */ + @Getter + private int shoalDuration = 0; + + /** + * -- GETTER -- + * Get the number of ticks the shoal has been stationary + */ + @Getter + private int stationaryTicks = 0; + + // Health-based movement tracking + private int previousHealthRatio = -1; + + // Depth tracking + /** + * -- GETTER -- + * Get the current shoal depth based on NPC animation + */ + @Getter + private ShoalDepth currentShoalDepth = ShoalDepth.UNKNOWN; + + /** + * -- GETTER -- + * Get the current shoal NPC (for rendering/highlighting) + */ + @Getter + private NPC currentShoalNpc; + + /** + * Creates a new ShoalTracker with the specified client. + * + * @param client the RuneLite client instance + */ + + @Inject + public ShoalTracker(Client client, Notifier notifier, SailingConfig config, BoatTracker boatTracker) { + this.client = client; + this.notifier = notifier; + this.config = config; + this.boatTracker = boatTracker; + } + + @Override + public boolean isEnabled(SailingConfig config) { + // Service component - always enabled + return true; + } + + @Override + public void startUp() { + log.debug("ShoalTracker started"); + } + + @Override + public void shutDown() { + log.debug("ShoalTracker shut down"); + clearState(); + } + + // Public API methods + + /** + * Gets all current shoal GameObjects for rendering/highlighting. + * + * @return a copy of the current shoal objects set + */ + public Set getShoalObjects() { + return new HashSet<>(shoalObjects.values()); + } + + /** + * Gets debug information about current shoal objects. + * + * @return a string describing current shoal objects and their IDs + */ + public String getShoalObjectsDebugInfo() { + if (shoalObjects.isEmpty()) { + return "No shoal objects"; + } + + StringBuilder sb = new StringBuilder("Shoal objects: "); + shoalObjects.forEach((id, obj) -> sb.append(String.format("ID=%d ", id))); + return sb.toString().trim(); + } + + /** + * Checks if any shoal is currently active. + * + * @return true if a shoal entity or objects are present, false otherwise + */ + public boolean hasShoal() { + boolean hasEntity = currentShoalEntity != null; + boolean hasObjects = !shoalObjects.isEmpty(); + return hasEntity || hasObjects; + } + + /** + * Checks if the shoal WorldEntity is valid and trackable. + * + * @return true if the shoal entity exists and has a valid camera focus, false otherwise + */ + public boolean isShoalEntityInvalid() { + return currentShoalEntity == null || currentShoalEntity.getCameraFocus() == null; + } + + /** + * Determine shoal depth based on animation ID + * @param animationId The animation ID to check + * @return The corresponding ShoalDepth + */ + public ShoalDepth getShoalDepthFromAnimation(int animationId) { + if (animationId == SHOAL_DEPTH_SHALLOW) { + return ShoalDepth.SHALLOW; + } else if (animationId == SHOAL_DEPTH_MODERATE) { + return ShoalDepth.MODERATE; + } else if (animationId == SHOAL_DEPTH_DEEP) { + return ShoalDepth.DEEP; + } else { + return ShoalDepth.UNKNOWN; + } + } + + private void updateShoalDepth() { + if (currentShoalNpc != null) { + updateDepthFromNpc(); + } else { + resetDepthToUnknown(); + } + } + + private void updateDepthFromNpc() { + int animationId = currentShoalNpc.getAnimation(); + ShoalDepth newDepth = getShoalDepthFromAnimation(animationId); + + if (newDepth != currentShoalDepth) { + checkDepthNotification(); + currentShoalDepth = newDepth; + } + } + + private void checkDepthNotification() + { + Boat boat = boatTracker.getBoat(); + if (boat == null || boat.getFishingNets().isEmpty()) { + return; + } + notifier.notify(config.notifyDepthChange(), "Shoal depth changed"); + } + + private void checkMovementNotification() + { + Boat boat = boatTracker.getBoat(); + if (boat == null) { + return; + } + notifier.notify(config.notifyShoalMove(), "Shoal started moving"); + } + + private void resetDepthToUnknown() { + if (currentShoalDepth != ShoalDepth.UNKNOWN) { + currentShoalDepth = ShoalDepth.UNKNOWN; + } + } + + /** + * Checks if the shoal depth is currently known. + * + * @return true if depth is not UNKNOWN, false otherwise + */ + public boolean isShoalDepthKnown() { + return currentShoalDepth != ShoalDepth.UNKNOWN; + } + + /** + * Updates the shoal location and tracks movement. + */ + public void updateLocation() { + updateLocationFromEntity(); + } + + private void updateLocationFromEntity() { + if (currentShoalEntity == null) { + return; + } + + LocalPoint localPos = currentShoalEntity.getCameraFocus(); + if (localPos != null) { + WorldPoint newLocation = WorldPoint.fromLocal(client, localPos); + updateLocationIfChanged(newLocation); + } + } + + private void updateLocationIfChanged(WorldPoint newLocation) { + if (newLocation == null) { + return; + } + + if (!newLocation.equals(currentLocation)) { + currentLocation = newLocation; + updateShoalDuration(); + } + } + + private void updateShoalDuration() { + shoalDuration = TrawlingData.FishingAreas.getStopDurationForLocation(currentLocation); + } + + @SuppressWarnings("unused") + @Subscribe + public void onGameTick(GameTick e) { + if (!hasShoal()) { + resetMovementTracking(); + return; + } + + updateLocation(); + updateShoalDepth(); + trackMovementByHealth(); + } + + private void trackMovementByHealth() { + if (currentShoalNpc == null) { + return; + } + + int currentHealthRatio = currentShoalNpc.getHealthRatio(); + + // Check if health dropped below 1 (indicating shoal is about to move) + if (previousHealthRatio >= 1 && currentHealthRatio < 1) { + checkMovementNotification(); + } + + previousHealthRatio = currentHealthRatio; + } + + /** + * Reset movement tracking state + */ + private void resetMovementTracking() { + // Movement tracking + stationaryTicks = 0; + previousHealthRatio = -1; + } + + // Event handlers + + @SuppressWarnings("unused") + @Subscribe + public void onNpcSpawned(NpcSpawned e) { + NPC npc = e.getNpc(); + if (isShoalNpc(npc)) { + handleShoalNpcSpawned(npc); + } + } + + @SuppressWarnings("unused") + @Subscribe + public void onNpcDespawned(NpcDespawned e) { + NPC npc = e.getNpc(); + if (npc == currentShoalNpc) { + handleShoalNpcDespawned(); + } + } + + private boolean isShoalNpc(NPC npc) { + return npc.getId() == SAILING_SHOAL_RIPPLES; + } + + private void handleShoalNpcSpawned(NPC npc) { + currentShoalNpc = npc; + previousHealthRatio = npc.getHealthRatio(); // Initialize health tracking + updateShoalDepth(); + } + + private void handleShoalNpcDespawned() { + currentShoalNpc = null; + previousHealthRatio = -1; // Reset health tracking + updateShoalDepth(); + } + + @SuppressWarnings("unused") + @Subscribe + public void onWorldEntitySpawned(WorldEntitySpawned e) { + WorldEntity entity = e.getWorldEntity(); + + if (isShoalWorldEntity(entity)) { + handleShoalWorldEntitySpawned(entity); + } + } + + private boolean isShoalWorldEntity(WorldEntity entity) { + return entity.getConfig() != null && entity.getConfig().getId() == SHOAL_WORLD_ENTITY_CONFIG_ID; + } + + private void handleShoalWorldEntitySpawned(WorldEntity entity) { + currentShoalEntity = entity; + updateLocation(); + } + + @SuppressWarnings("unused") + @Subscribe + public void onGameObjectSpawned(GameObjectSpawned e) { + GameObject obj = e.getGameObject(); + + if (isShoalGameObject(obj)) { + handleShoalGameObjectSpawned(obj); + } + } + + @SuppressWarnings("unused") + @Subscribe + public void onGameObjectDespawned(GameObjectDespawned e) { + GameObject obj = e.getGameObject(); + + if (isShoalGameObject(obj)) { + handleShoalGameObjectDespawned(obj); + } + } + + private boolean isShoalGameObject(GameObject obj) { + return SHOAL_OBJECT_IDS.contains(obj.getId()); + } + + private void handleShoalGameObjectSpawned(GameObject obj) { + int objectId = obj.getId(); + shoalObjects.put(objectId, obj); + log.debug("Shoal GameObject spawned: ID={}", objectId); + } + + private void handleShoalGameObjectDespawned(GameObject obj) { + int objectId = obj.getId(); + GameObject removed = shoalObjects.remove(objectId); + if (removed != null) { + log.debug("Shoal GameObject despawned: ID={}", objectId); + } + } + + @SuppressWarnings("unused") + @Subscribe + public void onWorldViewUnloaded(WorldViewUnloaded e) { + if (!e.getWorldView().isTopLevel()) { + return; + } + + if (shouldClearStateOnWorldViewUnload()) { + clearState(); + } + } + + private boolean shouldClearStateOnWorldViewUnload() { + if (isPlayerOrWorldViewInvalid()) { + log.debug("Top-level world view unloaded (player/worldview null), clearing shoal state"); + return true; + } + + if (!SailingUtil.isSailing(client)) { + log.debug("Top-level world view unloaded while not sailing, clearing shoal state"); + return true; + } + + return false; + } + + private boolean isPlayerOrWorldViewInvalid() { + return client.getLocalPlayer() == null || client.getLocalPlayer().getWorldView() == null; + } + + /** + * Attempts to find and set the current shoal WorldEntity. + */ + public void findShoalEntity() { + WorldEntity foundEntity = searchForShoalEntity(); + + if (foundEntity != null) { + handleFoundShoalEntity(foundEntity); + } else { + handleMissingShoalEntity(); + } + } + + private WorldEntity searchForShoalEntity() { + if (client.getTopLevelWorldView() == null) { + return null; + } + + for (WorldEntity entity : client.getTopLevelWorldView().worldEntities()) { + if (isShoalWorldEntity(entity)) { + return entity; + } + } + + return null; + } + + private void handleFoundShoalEntity(WorldEntity entity) { + currentShoalEntity = entity; + updateLocation(); + log.debug("Found shoal WorldEntity in scene"); + } + + private void handleMissingShoalEntity() { + if (currentShoalEntity != null) { + log.debug("Shoal WorldEntity no longer exists"); + clearShoalEntityState(); + } + } + + private void clearShoalEntityState() { + currentShoalEntity = null; + currentLocation = null; + shoalDuration = 0; + } + + + + /** + * Clear all tracking state + */ + private void clearState() { + currentShoalEntity = null; + shoalObjects.clear(); + currentLocation = null; + shoalDuration = 0; + currentShoalNpc = null; + currentShoalDepth = ShoalDepth.UNKNOWN; + resetMovementTracking(); + log.debug("ShoalTracker state cleared"); + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalWaypoint.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalWaypoint.java new file mode 100644 index 00000000..90b3b424 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/ShoalWaypoint.java @@ -0,0 +1,75 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.coords.WorldPoint; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a waypoint in a shoal's movement path. + * Contains position information and whether it's a stop point. + * Stop duration is area-specific, not waypoint-specific. + */ +@Getter +@RequiredArgsConstructor +public class ShoalWaypoint { + private final WorldPoint position; + private final boolean stopPoint; + + /** + * Convenience constructor for non-stop waypoints. + */ + public ShoalWaypoint(WorldPoint position) { + this(position, false); + } + + /** + * Extract all positions from an array of waypoints. + * Useful for compatibility with existing code that expects WorldPoint arrays. + */ + public static WorldPoint[] getPositions(ShoalWaypoint[] waypoints) { + return Arrays.stream(waypoints) + .map(ShoalWaypoint::getPosition) + .toArray(WorldPoint[]::new); + } + + /** + * Get indices of all stop points in the waypoint array. + * Useful for compatibility with existing code that uses stop index arrays. + */ + public static int[] getStopIndices(ShoalWaypoint[] waypoints) { + List indices = new ArrayList<>(); + for (int i = 0; i < waypoints.length; i++) { + if (waypoints[i].isStopPoint()) { + indices.add(i); + } + } + return indices.stream().mapToInt(Integer::intValue).toArray(); + } + + /** + * Get all stop point waypoints from the array. + */ + public static ShoalWaypoint[] getStopPoints(ShoalWaypoint[] waypoints) { + return Arrays.stream(waypoints) + .filter(ShoalWaypoint::isStopPoint) + .toArray(ShoalWaypoint[]::new); + } + + /** + * Count the number of stop points in the waypoint array. + */ + public static int getStopPointCount(ShoalWaypoint[] waypoints) { + return (int) Arrays.stream(waypoints) + .filter(ShoalWaypoint::isStopPoint) + .count(); + } + + @Override + public String toString() { + return String.format("ShoalWaypoint{position=%s, stopPoint=%s}", position, stopPoint); + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/TrawlingData.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/TrawlingData.java new file mode 100644 index 00000000..79d7e899 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/TrawlingData.java @@ -0,0 +1,61 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.model.FishingAreaType; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.ObjectID; + +class TrawlingData { + + static class ShoalObjectID { + static final int GIANT_KRILL = ObjectID.SAILING_SHOAL_CLICKBOX_GIANT_KRILL; + static final int HADDOCK = ObjectID.SAILING_SHOAL_CLICKBOX_HADDOCK; + static final int YELLOWFIN = ObjectID.SAILING_SHOAL_CLICKBOX_YELLOWFIN; + static final int HALIBUT = ObjectID.SAILING_SHOAL_CLICKBOX_HALIBUT; + static final int BLUEFIN = ObjectID.SAILING_SHOAL_CLICKBOX_BLUEFIN; + static final int MARLIN = ObjectID.SAILING_SHOAL_CLICKBOX_MARLIN; + static final int SHIMMERING = ObjectID.SAILING_SHOAL_CLICKBOX_SHIMMERING; + static final int GLISTENING = ObjectID.SAILING_SHOAL_CLICKBOX_GLISTENING; + static final int VIBRANT = ObjectID.SAILING_SHOAL_CLICKBOX_VIBRANT; + } + + static class FishingAreas { + + /** + * Get the fishing area type for a given world location + * @param location The world point to check + * @return The fishing area type, or null if not in a known fishing area + */ + static FishingAreaType getFishingAreaType(final WorldPoint location) { + if (location == null) { + return null; + } + + for (final var area : ShoalFishingArea.AREAS) { + if (area.contains(location)) { + return area.getShoal().getDepth(); + } + } + + return null; + } + + /** + * Get the shoal stop duration for a given world location + * @param location The world point to check + * @return The stop duration in ticks, or -1 if not in a known fishing area + */ + static int getStopDurationForLocation(final WorldPoint location) { + if (location == null) { + return -1; + } + + for (final var area : ShoalFishingArea.AREAS) { + if (area.contains(location)) { + return area.getShoal().getStopDuration(); + } + } + + return -1; + } + } +} diff --git a/src/main/java/com/duckblade/osrs/sailing/features/trawling/TrawlingOverlay.java b/src/main/java/com/duckblade/osrs/sailing/features/trawling/TrawlingOverlay.java new file mode 100644 index 00000000..19f56be7 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/features/trawling/TrawlingOverlay.java @@ -0,0 +1,145 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.features.util.SailingUtil; +import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.TitleComponent; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import org.apache.commons.text.WordUtils; + +/** + * Combined overlay for trawling features including net capacity and fish caught + */ +@Slf4j +@Singleton +public class TrawlingOverlay extends OverlayPanel + implements PluginLifecycleComponent { + + private final Client client; + private final FishCaughtTracker fishCaughtTracker; + private final SailingConfig config; + + @Inject + public TrawlingOverlay(Client client, FishCaughtTracker fishCaughtTracker, SailingConfig config) { + this.client = client; + this.fishCaughtTracker = fishCaughtTracker; + this.config = config; + setPosition(OverlayPosition.TOP_LEFT); + } + + @Override + public boolean isEnabled(SailingConfig config) { + // Enable if either feature is enabled + return config.trawlingShowNetCapacity() || config.trawlingShowFishCaught(); + } + + @Override + public void startUp() { + log.debug("TrawlingOverlay started"); + } + + @Override + public void shutDown() { + log.debug("TrawlingOverlay shut down"); + } + + @Override + public Dimension render(Graphics2D graphics) { + if (!SailingUtil.isSailing(client)) { + return null; + } + + panelComponent.getChildren().clear(); + boolean hasContent = false; + + + + // Add fish caught section if enabled and available + if (shouldShowFishCaught()) { + var fishCaught = fishCaughtTracker.getFishCaught(); + if (!fishCaught.isEmpty()) { + if (hasContent) { + panelComponent.getChildren().add(LineComponent.builder().build()); + } + + int totalFish = fishCaught.values().stream().reduce(Integer::sum).orElse(0); + for (var entry : fishCaught.entrySet()) { + var shoal = entry.getKey(); + panelComponent.getChildren().add(LineComponent.builder() + .leftColor(shoal.getColor()) + .left(shoal.getName()) + .right(String.format("%d (%.0f%%)", entry.getValue(), 100f * entry.getValue() / totalFish)) + .build()); + } + + panelComponent.getChildren().add(LineComponent.builder() + .left("TOTAL") + .right(String.valueOf(totalFish)) + .build()); + + hasContent = true; + } + } + + // Add net capacity section if enabled and available + if (shouldShowNetCapacity()) { + int maxCapacity = fishCaughtTracker.getNetCapacity(); + if (maxCapacity > 0) { + if (hasContent) { + panelComponent.getChildren().add(LineComponent.builder().build()); + } + + int fishInNetTotal = fishCaughtTracker.getFishInNetCount(); + + // Choose color based on how full the nets are + Color textColor; + float fillPercent = (float) fishInNetTotal / maxCapacity; + if (fillPercent >= 0.9f) { + textColor = Color.RED; // Nearly full + } else if (fillPercent >= 0.7f) { + textColor = Color.ORANGE; // Getting full + } else { + textColor = Color.WHITE; // Plenty of space + } + + panelComponent.getChildren().add(LineComponent.builder() + .left("Net:") + .right(fishInNetTotal + "/" + maxCapacity) + .rightColor(textColor) + .build()); + + hasContent = true; + } + } + + if (hasContent) { + panelComponent.getChildren().add(0, TitleComponent.builder() + .text("Trawling") + .color(Color.CYAN) + .build()); + + return super.render(graphics); + } + + return null; + } + + private boolean shouldShowNetCapacity() { + return config.trawlingShowNetCapacity(); + } + + private boolean shouldShowFishCaught() { + return config.trawlingShowFishCaught(); + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/features/util/BoatTracker.java b/src/main/java/com/duckblade/osrs/sailing/features/util/BoatTracker.java index 5bf996ee..649737af 100644 --- a/src/main/java/com/duckblade/osrs/sailing/features/util/BoatTracker.java +++ b/src/main/java/com/duckblade/osrs/sailing/features/util/BoatTracker.java @@ -1,11 +1,14 @@ package com.duckblade.osrs.sailing.features.util; - import com.duckblade.osrs.sailing.model.Boat; import com.duckblade.osrs.sailing.model.CargoHoldTier; +import com.duckblade.osrs.sailing.model.ChumStationTier; import com.duckblade.osrs.sailing.model.HelmTier; import com.duckblade.osrs.sailing.model.HullTier; import com.duckblade.osrs.sailing.model.SailTier; import com.duckblade.osrs.sailing.model.SalvagingHookTier; +import com.duckblade.osrs.sailing.model.FishingNetTier; +import com.duckblade.osrs.sailing.model.CannonTier; +import com.duckblade.osrs.sailing.model.WindCatcherTier; import com.duckblade.osrs.sailing.module.PluginLifecycleComponent; import java.util.HashMap; import java.util.Map; @@ -93,6 +96,26 @@ public void onGameObjectSpawned(GameObjectSpawned e) boat.setCargoHold(o); log.trace("found cargo hold {}={} for boat in wv {}", o.getId(), boat.getCargoHoldTier(), boat.getWorldViewId()); } + if (ChumStationTier.fromGameObjectId(o.getId()) != null) + { + boat.setChumStation(o); + log.trace("found chum station {}={} for boat in wv {}", o.getId(), boat.getChumStationTier(), boat.getWorldViewId()); + } + if (FishingNetTier.fromGameObjectId(o.getId()) != null) + { + boat.getFishingNets().add(o); + log.trace("found fishing net {}={} for boat in wv {}", o.getId(), FishingNetTier.fromGameObjectId(o.getId()), boat.getWorldViewId()); + } + if (CannonTier.fromGameObjectId(o.getId()) != null) + { + boat.getCannons().add(o); + log.trace("found cannon {}={} for boat in wv {}", o.getId(), CannonTier.fromGameObjectId(o.getId()), boat.getWorldViewId()); + } + if (WindCatcherTier.fromGameObjectId(o.getId()) != null) + { + boat.setWindCatcher(o); + log.trace("found wind catcher {}={} for boat in wv {}", o.getId(), boat.getWindCatcherTier(), boat.getWorldViewId()); + } } @Subscribe @@ -129,10 +152,32 @@ public void onGameObjectDespawned(GameObjectDespawned e) boat.setCargoHold(null); log.trace("unsetting cargo hold for boat in wv {}", boat.getWorldViewId()); } + if (boat.getChumStation() == o) + { + boat.setChumStation(null); + log.trace("unsetting chum station for boat in wv {}", boat.getWorldViewId()); + } + if (boat.getFishingNets().remove(o)) + { + log.trace("unsetting fishing net for boat in wv {}", boat.getWorldViewId()); + } + if (boat.getCannons().remove(o)) + { + log.trace("unsetting cannon for boat in wv {}", boat.getWorldViewId()); + } + if (boat.getWindCatcher() == o) + { + boat.setWindCatcher(null); + log.trace("unsetting wind catcher for boat in wv {}", boat.getWorldViewId()); + } } public Boat getBoat() { + if (client.getLocalPlayer() == null) + { + return null; + } return getBoat(client.getLocalPlayer().getWorldView().getId()); } diff --git a/src/main/java/com/duckblade/osrs/sailing/model/Boat.java b/src/main/java/com/duckblade/osrs/sailing/model/Boat.java index 4bb0168f..2f397bed 100644 --- a/src/main/java/com/duckblade/osrs/sailing/model/Boat.java +++ b/src/main/java/com/duckblade/osrs/sailing/model/Boat.java @@ -23,9 +23,13 @@ public class Boat GameObject sail; GameObject helm; GameObject cargoHold; + GameObject chumStation; + GameObject windCatcher; @Setter(AccessLevel.NONE) Set salvagingHooks = new HashSet<>(); + Set fishingNets = new HashSet<>(); + Set cannons = new HashSet<>(); // these are intentionally not cached in case the object is transformed without respawning // e.g. helms have a different idle vs in-use id @@ -44,6 +48,11 @@ public HelmTier getHelmTier() return helm != null ? HelmTier.fromGameObjectId(helm.getId()) : null; } + public WindCatcherTier getWindCatcherTier() + { + return windCatcher != null ? WindCatcherTier.fromGameObjectId(windCatcher.getId()) : null; + } + public List getSalvagingHookTiers() { return salvagingHooks.stream() @@ -52,11 +61,32 @@ public List getSalvagingHookTiers() .collect(Collectors.toList()); } + public List getNetTiers() + { + return fishingNets.stream() + .mapToInt(GameObject::getId) + .mapToObj(FishingNetTier::fromGameObjectId) + .collect(Collectors.toList()); + } + + public List getCannonTiers() + { + return cannons.stream() + .mapToInt(GameObject::getId) + .mapToObj(CannonTier::fromGameObjectId) + .collect(Collectors.toList()); + } + public CargoHoldTier getCargoHoldTier() { return cargoHold != null ? CargoHoldTier.fromGameObjectId(cargoHold.getId()) : null; } + public ChumStationTier getChumStationTier() + { + return chumStation != null ? ChumStationTier.fromGameObjectId(chumStation.getId()) : null; + } + public SizeClass getSizeClass() { return hull != null ? SizeClass.fromGameObjectId(hull.getId()) : null; @@ -70,6 +100,11 @@ public Set getAllFacilities() facilities.add(helm); facilities.addAll(salvagingHooks); facilities.add(cargoHold); + facilities.add(chumStation); + facilities.addAll(fishingNets); + facilities.addAll(cannons); + facilities.add(windCatcher); + facilities.addAll(fishingNets); return facilities; } @@ -84,6 +119,11 @@ public int getCargoCapacity() return cargoHoldTier.getCapacity(getSizeClass()); } + public int getNetCapacity() + { + return fishingNets.size() * 125; + } + public int getSpeedBoostDuration() { SailTier sailTier = getSailTier(); @@ -98,7 +138,7 @@ public int getSpeedBoostDuration() public String getDebugString() { return String.format( - "Id: %d, Hull: %s, Sail: %s, Helm: %s, Hook: %s, Cargo: %s", + "Id: %d, Hull: %s, Sail: %s, Helm: %s, Hook: %s, Cargo: %s, Chum: %s, Nets: %s, Cannons: %s, WindCatcher: %s", worldViewId, getHullTier(), getSailTier(), @@ -107,7 +147,17 @@ public String getDebugString() .stream() .map(SalvagingHookTier::toString) .collect(Collectors.joining(", ", "[", "]")), - getCargoHoldTier() + getCargoHoldTier(), + getChumStationTier(), + getNetTiers() + .stream() + .map(FishingNetTier::toString) + .collect(Collectors.joining(", ", "[", "]")), + getCannonTiers() + .stream() + .map(CannonTier::toString) + .collect(Collectors.joining(", ", "[", "]")), + getWindCatcherTier() ); } } diff --git a/src/main/java/com/duckblade/osrs/sailing/model/CannonTier.java b/src/main/java/com/duckblade/osrs/sailing/model/CannonTier.java new file mode 100644 index 00000000..09af35c1 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/model/CannonTier.java @@ -0,0 +1,72 @@ +package com.duckblade.osrs.sailing.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.gameval.ObjectID; + +@RequiredArgsConstructor +@Getter +public enum CannonTier +{ + + BRONZE( + new int[]{ + ObjectID.SAILING_BRONZE_CANNON + } + ), + IRON( + new int[]{ + ObjectID.SAILING_IRON_CANNON + } + ), + STEEL( + new int[]{ + ObjectID.SAILING_STEEL_CANNON + } + ), + MITHRIL( + new int[]{ + ObjectID.SAILING_MITHRIL_CANNON + } + ), + ADAMANT( + new int[]{ + ObjectID.SAILING_ADAMANT_CANNON + } + ), + RUNE( + new int[]{ + ObjectID.SAILING_RUNE_CANNON + } + ), + DRAGON( + new int[]{ + ObjectID.SAILING_DRAGON_CANNON + } + ), + GOLD( + new int[]{ + ObjectID.SAILING_GOLD_CANNON + } + ), + ; + + private final int[] gameObjectIds; + + public static CannonTier fromGameObjectId(int id) + { + for (CannonTier tier : values()) + { + for (int objectId : tier.getGameObjectIds()) + { + if (objectId == id) + { + return tier; + } + } + } + + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/model/ChumStationTier.java b/src/main/java/com/duckblade/osrs/sailing/model/ChumStationTier.java new file mode 100644 index 00000000..c952d172 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/model/ChumStationTier.java @@ -0,0 +1,58 @@ +package com.duckblade.osrs.sailing.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.gameval.ObjectID; + +@RequiredArgsConstructor +@Getter +public enum ChumStationTier +{ + + + BASIC( + new int[]{ + ObjectID.CHUM_STATION_2X5A, + ObjectID.CHUM_STATION_2X5B, + ObjectID.CHUM_STATION_3X8A, + ObjectID.CHUM_STATION_3X8B + } + ), + // look at these variables and tell me you dont see it I dare you + ADVANCED( + new int[]{ + ObjectID.CHUM_SPREADER_2X5A, + ObjectID.CHUM_SPREADER_2X5B, + ObjectID.CHUM_SPREADER_3X8A, + ObjectID.CHUM_SPREADER_3X8B + } + ), + SPREADER( + new int[]{ + ObjectID.CHUM_STATION_ADVANCED_2X5A, + ObjectID.CHUM_STATION_ADVANCED_2X5B, + ObjectID.CHUM_STATION_ADVANCED_3X8A, + ObjectID.CHUM_STATION_ADVANCED_3X8B + } + ), + ; + + private final int[] gameObjectIds; + + public static ChumStationTier fromGameObjectId(int id) + { + for (ChumStationTier tier : values()) + { + for (int objectId : tier.getGameObjectIds()) + { + if (objectId == id) + { + return tier; + } + } + } + + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/model/FishingAreaType.java b/src/main/java/com/duckblade/osrs/sailing/model/FishingAreaType.java new file mode 100644 index 00000000..d05ae0f2 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/model/FishingAreaType.java @@ -0,0 +1,10 @@ +package com.duckblade.osrs.sailing.model; + +/** + * Represents the type of fishing area based on depth patterns + */ +public enum FishingAreaType { + ONE_DEPTH, // Krill, Haddock + TWO_DEPTH, // Standard areas (e.g., Yellowfin, Halibut) + THREE_DEPTH // Special areas (Bluefin, Marlin) +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/model/FishingNetTier.java b/src/main/java/com/duckblade/osrs/sailing/model/FishingNetTier.java new file mode 100644 index 00000000..275b52d0 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/model/FishingNetTier.java @@ -0,0 +1,58 @@ +package com.duckblade.osrs.sailing.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.gameval.ObjectID; + +@RequiredArgsConstructor +@Getter +public enum FishingNetTier { + ROPE( + new int[]{ + ObjectID.SAILING_ROPE_TRAWLING_NET, + ObjectID.SAILING_ROPE_TRAWLING_NET_3X8_PORT, + ObjectID.SAILING_ROPE_TRAWLING_NET_3X8_STARBOARD + } + ), + LINEN(new int[]{ + ObjectID.SAILING_LINEN_TRAWLING_NET, + ObjectID.SAILING_LINEN_TRAWLING_NET_3X8_PORT, + ObjectID.SAILING_LINEN_TRAWLING_NET_3X8_STARBOARD + } + ), + HEMP(new int[]{ + ObjectID.SAILING_HEMP_TRAWLING_NET, + ObjectID.SAILING_HEMP_TRAWLING_NET_3X8_PORT, + ObjectID.SAILING_HEMP_TRAWLING_NET_3X8_STARBOARD, + } + ), + COTTON(new int[]{ + ObjectID.SAILING_COTTON_TRAWLING_NET, + ObjectID.SAILING_COTTON_TRAWLING_NET_3X8_PORT, + ObjectID.SAILING_COTTON_TRAWLING_NET_3X8_STARBOARD, + } + ); + + private final int[] gameObjectIds; + + public static FishingNetTier fromGameObjectId(int id) + { + for (FishingNetTier tier : values()) + { + for (int objectId : tier.getGameObjectIds()) + { + if (objectId == id) + { + return tier; + } + } + } + return null; + } + + public int getCapacity() { + return 125; + } +} + + diff --git a/src/main/java/com/duckblade/osrs/sailing/model/ShoalDepth.java b/src/main/java/com/duckblade/osrs/sailing/model/ShoalDepth.java new file mode 100644 index 00000000..d9e236c3 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/model/ShoalDepth.java @@ -0,0 +1,8 @@ +package com.duckblade.osrs.sailing.model; + +public enum ShoalDepth { + SHALLOW, + MODERATE, + DEEP, + UNKNOWN +} diff --git a/src/main/java/com/duckblade/osrs/sailing/model/WindCatcherTier.java b/src/main/java/com/duckblade/osrs/sailing/model/WindCatcherTier.java new file mode 100644 index 00000000..6acbd25e --- /dev/null +++ b/src/main/java/com/duckblade/osrs/sailing/model/WindCatcherTier.java @@ -0,0 +1,44 @@ +package com.duckblade.osrs.sailing.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.gameval.ObjectID; + +@RequiredArgsConstructor +@Getter +public enum WindCatcherTier +{ + + WIND( + new int[]{ + ObjectID.SAILING_WIND_CATCHER_ACTIVATED, + ObjectID.SAILING_WIND_CATCHER_DEACTIVATED + } + ), + GALE( + new int[]{ + ObjectID.SAILING_GALE_CATCHER_ACTIVATED, + ObjectID.SAILING_GALE_CATCHER_DEACTIVATED + } + ), + ; + + private final int[] gameObjectIds; + + public static WindCatcherTier fromGameObjectId(int id) + { + for (WindCatcherTier tier : values()) + { + for (int objectId : tier.getGameObjectIds()) + { + if (objectId == id) + { + return tier; + } + } + } + + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/sailing/module/ComponentManager.java b/src/main/java/com/duckblade/osrs/sailing/module/ComponentManager.java index 542962c2..eb0f1b0e 100644 --- a/src/main/java/com/duckblade/osrs/sailing/module/ComponentManager.java +++ b/src/main/java/com/duckblade/osrs/sailing/module/ComponentManager.java @@ -64,7 +64,7 @@ public void onConfigChanged(ConfigChanged e) revalidateComponentStates(); } - private void revalidateComponentStates() + public void revalidateComponentStates() { components.forEach(c -> { diff --git a/src/main/java/com/duckblade/osrs/sailing/module/SailingModule.java b/src/main/java/com/duckblade/osrs/sailing/module/SailingModule.java index f60bbf5b..81cf389d 100644 --- a/src/main/java/com/duckblade/osrs/sailing/module/SailingModule.java +++ b/src/main/java/com/duckblade/osrs/sailing/module/SailingModule.java @@ -1,6 +1,8 @@ package com.duckblade.osrs.sailing.module; import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.features.charting.*; +import com.duckblade.osrs.sailing.features.reversebeep.ReverseBeep; import com.duckblade.osrs.sailing.features.barracudatrials.HidePortalTransitions; import com.duckblade.osrs.sailing.features.barracudatrials.JubblyJiveHelper; import com.duckblade.osrs.sailing.features.barracudatrials.LostCargoHighlighter; @@ -9,13 +11,6 @@ import com.duckblade.osrs.sailing.features.barracudatrials.splits.BarracudaSplitsFileWriter; import com.duckblade.osrs.sailing.features.barracudatrials.splits.BarracudaSplitsOverlayPanel; import com.duckblade.osrs.sailing.features.barracudatrials.splits.BarracudaSplitsTracker; -import com.duckblade.osrs.sailing.features.charting.CurrentDuckTaskTracker; -import com.duckblade.osrs.sailing.features.charting.MermaidTaskSolver; -import com.duckblade.osrs.sailing.features.charting.SeaChartMapPointManager; -import com.duckblade.osrs.sailing.features.charting.SeaChartOverlay; -import com.duckblade.osrs.sailing.features.charting.SeaChartPanelOverlay; -import com.duckblade.osrs.sailing.features.charting.SeaChartTaskIndex; -import com.duckblade.osrs.sailing.features.charting.WeatherTaskTracker; import com.duckblade.osrs.sailing.features.courier.CourierDestinationOverlay; import com.duckblade.osrs.sailing.features.courier.CourierTaskLedgerOverlay; import com.duckblade.osrs.sailing.features.courier.CourierTaskTracker; @@ -39,8 +34,18 @@ import com.duckblade.osrs.sailing.features.oceanencounters.LostShipment; import com.duckblade.osrs.sailing.features.oceanencounters.MysteriousGlow; import com.duckblade.osrs.sailing.features.oceanencounters.OceanMan; -import com.duckblade.osrs.sailing.features.reversebeep.ReverseBeep; import com.duckblade.osrs.sailing.features.salvaging.SalvagingHighlight; +import com.duckblade.osrs.sailing.features.trawling.FishCaughtTracker; +import com.duckblade.osrs.sailing.features.trawling.NetDepthButtonHighlighter; +import com.duckblade.osrs.sailing.features.trawling.NetDepthTimer; +import com.duckblade.osrs.sailing.features.trawling.NetDepthTracker; +import com.duckblade.osrs.sailing.features.trawling.TrawlingOverlay; +import com.duckblade.osrs.sailing.features.trawling.ShoalOverlay; +import com.duckblade.osrs.sailing.features.trawling.ShoalTracker; +import com.duckblade.osrs.sailing.features.trawling.ShoalPathTrackerOverlay; +import com.duckblade.osrs.sailing.features.trawling.ShoalPathTracker; +import com.duckblade.osrs.sailing.features.trawling.ShoalPathTrackerCommand; +import com.duckblade.osrs.sailing.features.trawling.ShoalPathOverlay; import com.duckblade.osrs.sailing.features.util.BoatTracker; import com.google.common.collect.ImmutableSet; import com.google.inject.AbstractModule; @@ -81,6 +86,7 @@ Set lifecycleComponents( CrystalExtractorHighlight crystalExtractorHighlight, CurrentDuckTaskTracker currentDuckTaskTracker, DeprioSailsOffHelm deprioSailsOffHelm, + FishCaughtTracker fishCaughtTracker, GiantClam giantClam, HidePortalTransitions hidePortalTransitions, HideStopNavigatingDuringTrials hideStopNavigatingDuringTrials, @@ -92,19 +98,29 @@ Set lifecycleComponents( LuffOverlay luffOverlay, MermaidTaskSolver mermaidTaskSolver, MysteriousGlow mysteriousGlow, + NetDepthButtonHighlighter netDepthButtonHighlighter, + NetDepthTimer netDepthTimer, + NetDepthTracker netDepthTracker, NavigationOverlay navigationOverlay, OceanMan oceanMan, PrioritizeCargoHold prioritizeCargoHold, RapidsOverlay rapidsOverlay, ReverseBeep reverseBeep, SalvagingHighlight salvagingHighlight, - SeaChartMapPointManager seaChartMapPointManager, + SeaChartMapPointManager seaChartMapPointManager, SeaChartOverlay seaChartOverlay, SeaChartPanelOverlay seaChartPanelOverlay, SeaChartTaskIndex seaChartTaskIndex, + ShoalOverlay shoalOverlay, + ShoalPathTrackerOverlay shoalPathTrackerOverlay, + ShoalPathTracker shoalPathTracker, + ShoalPathTrackerCommand shoalPathTrackerCommand, + ShoalPathOverlay shoalPathOverlay, + ShoalTracker shoalTracker, SpeedBoostInfoBox speedBoostInfoBox, TemporTantrumHelper temporTantrumHelper, TrueTileIndicator trueTileIndicator, + TrawlingOverlay trawlingOverlay, WeatherTaskTracker weatherTaskTracker ) { @@ -114,7 +130,7 @@ Set lifecycleComponents( .add(barracudaSplitsOverlayPanel) .add(barracudaSplitsFileWriter) .add(boatTracker) - .add(cargoHoldTracker) + .add(cargoHoldTracker) .add(castaway) .add(clueCasket) .add(clueTurtle) @@ -136,6 +152,11 @@ Set lifecycleComponents( .add(luffOverlay) .add(mermaidTaskSolver) .add(mysteriousGlow) + .add(fishCaughtTracker) + .add(netDepthButtonHighlighter) + .add(netDepthTimer) + .add(netDepthTracker) + .add(trawlingOverlay) .add(navigationOverlay) .add(oceanMan) .add(prioritizeCargoHold) @@ -143,18 +164,24 @@ Set lifecycleComponents( .add(reverseBeep) .add(salvagingHighlight) .add(seaChartOverlay) - .add(seaChartMapPointManager) + .add(seaChartMapPointManager) .add(seaChartPanelOverlay) .add(seaChartTaskIndex) .add(speedBoostInfoBox) + .add(shoalOverlay) + .add(shoalPathOverlay) + .add(shoalPathTracker) + .add(shoalTracker) .add(temporTantrumHelper) .add(trueTileIndicator) .add(weatherTaskTracker); // features still in development - //noinspection StatementWithEmptyBody if (developerMode) { + builder + .add(shoalPathTrackerCommand) + .add(shoalPathTrackerOverlay); } return builder.build(); diff --git a/src/test/java/com/duckblade/osrs/sailing/debugplugin/features/FacilitiesOverlay.java b/src/test/java/com/duckblade/osrs/sailing/debugplugin/features/FacilitiesOverlay.java index 1690d302..304ee542 100644 --- a/src/test/java/com/duckblade/osrs/sailing/debugplugin/features/FacilitiesOverlay.java +++ b/src/test/java/com/duckblade/osrs/sailing/debugplugin/features/FacilitiesOverlay.java @@ -6,6 +6,8 @@ import com.duckblade.osrs.sailing.features.util.SailingUtil; import com.duckblade.osrs.sailing.model.Boat; import com.duckblade.osrs.sailing.model.SalvagingHookTier; +import com.duckblade.osrs.sailing.model.CannonTier; +import com.duckblade.osrs.sailing.model.WindCatcherTier; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; @@ -61,10 +63,15 @@ public Dimension render(Graphics2D graphics) renderFacility(graphics, Color.CYAN, "sail", boat.getSail(), boat.getSailTier()); renderFacility(graphics, Color.ORANGE, "helm", boat.getHelm(), boat.getHelmTier()); renderFacility(graphics, Color.GREEN, "cargo", boat.getCargoHold(), boat.getCargoHoldTier()); + renderFacility(graphics, Color.MAGENTA, "windcatcher", boat.getWindCatcher(), boat.getWindCatcherTier()); for (GameObject hook : boat.getSalvagingHooks()) { renderFacility(graphics, Color.RED, "hook", hook, SalvagingHookTier.fromGameObjectId(hook.getId())); } + for (GameObject cannon : boat.getCannons()) + { + renderFacility(graphics, Color.YELLOW, "cannon", cannon, CannonTier.fromGameObjectId(cannon.getId())); + } return null; } diff --git a/src/test/java/com/duckblade/osrs/sailing/debugplugin/features/LocalBoatInfoOverlayPanel.java b/src/test/java/com/duckblade/osrs/sailing/debugplugin/features/LocalBoatInfoOverlayPanel.java index 3630f20c..045a0724 100644 --- a/src/test/java/com/duckblade/osrs/sailing/debugplugin/features/LocalBoatInfoOverlayPanel.java +++ b/src/test/java/com/duckblade/osrs/sailing/debugplugin/features/LocalBoatInfoOverlayPanel.java @@ -4,7 +4,10 @@ import com.duckblade.osrs.sailing.debugplugin.module.DebugLifecycleComponent; import com.duckblade.osrs.sailing.features.util.BoatTracker; import com.duckblade.osrs.sailing.model.Boat; +import com.duckblade.osrs.sailing.model.FishingNetTier; import com.duckblade.osrs.sailing.model.SalvagingHookTier; +import com.duckblade.osrs.sailing.model.CannonTier; +import com.duckblade.osrs.sailing.model.WindCatcherTier; import java.awt.Dimension; import java.awt.Graphics2D; import java.util.stream.Collectors; @@ -93,6 +96,38 @@ public Dimension render(Graphics2D graphics) .right(String.valueOf(boat.getCargoHoldTier())) .build()); + getPanelComponent().getChildren() + .add(LineComponent.builder() + .left("Chum") + .right(String.valueOf(boat.getChumStationTier())) + .build()); + + getPanelComponent().getChildren() + .add(LineComponent.builder() + .left("Nets") + .right(boat + .getNetTiers() + .stream() + .map(FishingNetTier::toString) + .collect(Collectors.joining(", ", "[", "]"))) + .build()); + + getPanelComponent().getChildren() + .add(LineComponent.builder() + .left("Cannons") + .right(boat + .getCannonTiers() + .stream() + .map(CannonTier::toString) + .collect(Collectors.joining(", ", "[", "]"))) + .build()); + + getPanelComponent().getChildren() + .add(LineComponent.builder() + .left("WindCatcher") + .right(String.valueOf(boat.getWindCatcherTier())) + .build()); + return super.render(graphics); } diff --git a/src/test/java/com/duckblade/osrs/sailing/features/trawling/NetDepthTrackerTest.java b/src/test/java/com/duckblade/osrs/sailing/features/trawling/NetDepthTrackerTest.java new file mode 100644 index 00000000..1e9f8a4f --- /dev/null +++ b/src/test/java/com/duckblade/osrs/sailing/features/trawling/NetDepthTrackerTest.java @@ -0,0 +1,223 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.model.ShoalDepth; +import net.runelite.api.Client; +import net.runelite.api.events.VarbitChanged; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Tests for NetDepthTracker + */ +public class NetDepthTrackerTest { + + @Mock + private Client client; + + @Mock + private VarbitChanged varbitChanged; + + private NetDepthTracker tracker; + + private static final int TRAWLING_NET_PORT_VARBIT = 19208; // VarbitID.SAILING_SIDEPANEL_BOAT_TRAWLING_NET_1_DEPTH + private static final int TRAWLING_NET_STARBOARD_VARBIT = 19206; // VarbitID.SAILING_SIDEPANEL_BOAT_TRAWLING_NET_0_DEPTH + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + tracker = new NetDepthTracker(client); + } + + @Test + public void testGetPortNetDepth_shallow() { + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(1); + + ShoalDepth result = tracker.getPortNetDepth(); + + assertEquals(ShoalDepth.SHALLOW, result); + } + + @Test + public void testGetPortNetDepth_moderate() { + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(2); + + ShoalDepth result = tracker.getPortNetDepth(); + + assertEquals(ShoalDepth.MODERATE, result); + } + + @Test + public void testGetPortNetDepth_deep() { + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(3); + + ShoalDepth result = tracker.getPortNetDepth(); + + assertEquals(ShoalDepth.DEEP, result); + } + + @Test + public void testGetStarboardNetDepth_shallow() { + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(1); + + ShoalDepth result = tracker.getStarboardNetDepth(); + + assertEquals(ShoalDepth.SHALLOW, result); + } + + @Test + public void testGetStarboardNetDepth_moderate() { + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(2); + + ShoalDepth result = tracker.getStarboardNetDepth(); + + assertEquals(ShoalDepth.MODERATE, result); + } + + @Test + public void testGetStarboardNetDepth_deep() { + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(3); + + ShoalDepth result = tracker.getStarboardNetDepth(); + + assertEquals(ShoalDepth.DEEP, result); + } + + @Test + public void testAreNetsAtSameDepth_true() { + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(2); + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(2); + + boolean result = tracker.areNetsAtSameDepth(); + + assertTrue(result); + } + + @Test + public void testAreNetsAtSameDepth_false() { + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(1); + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(3); + + boolean result = tracker.areNetsAtSameDepth(); + + assertFalse(result); + } + + @Test + public void testAreNetsAtDepth_true() { + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(2); + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(2); + + boolean result = tracker.areNetsAtDepth(ShoalDepth.MODERATE); + + assertTrue(result); + } + + @Test + public void testAreNetsAtDepth_false_portDifferent() { + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(1); + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(2); + + boolean result = tracker.areNetsAtDepth(ShoalDepth.MODERATE); + + assertFalse(result); + } + + @Test + public void testAreNetsAtDepth_false_starboardDifferent() { + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(2); + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(3); + + boolean result = tracker.areNetsAtDepth(ShoalDepth.MODERATE); + + assertFalse(result); + } + + @Test + public void testOnVarbitChanged_portNet() { + // Setup initial state + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(1); + tracker.startUp(); // Initialize cached values + + // Change port net depth + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(3); + when(varbitChanged.getVarbitId()).thenReturn(TRAWLING_NET_PORT_VARBIT); + when(varbitChanged.getValue()).thenReturn(3); + + tracker.onVarbitChanged(varbitChanged); + + assertEquals(ShoalDepth.DEEP, tracker.getPortNetDepth()); + } + + @Test + public void testOnVarbitChanged_starboardNet() { + // Setup initial state + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(2); + tracker.startUp(); // Initialize cached values + + // Change starboard net depth + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(1); + when(varbitChanged.getVarbitId()).thenReturn(TRAWLING_NET_STARBOARD_VARBIT); + when(varbitChanged.getValue()).thenReturn(1); + + tracker.onVarbitChanged(varbitChanged); + + assertEquals(ShoalDepth.SHALLOW, tracker.getStarboardNetDepth()); + } + + @Test + public void testOnVarbitChanged_unrelatedVarbit() { + // Setup initial state + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(2); + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(2); + tracker.startUp(); // Initialize cached values + + // Trigger unrelated varbit change + when(varbitChanged.getVarbitId()).thenReturn(99999); + when(varbitChanged.getValue()).thenReturn(5); + + tracker.onVarbitChanged(varbitChanged); + + // Values should remain unchanged + assertEquals(ShoalDepth.MODERATE, tracker.getPortNetDepth()); + assertEquals(ShoalDepth.MODERATE, tracker.getStarboardNetDepth()); + } + + @Test + public void testInvalidVarbitValue() { + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(99); + + ShoalDepth result = tracker.getPortNetDepth(); + + assertNull(result); + } + + @Test + public void testShutDown() { + // Setup some state + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(2); + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(3); + tracker.startUp(); + + // Verify state is set + assertEquals(ShoalDepth.MODERATE, tracker.getPortNetDepth()); + assertEquals(ShoalDepth.DEEP, tracker.getStarboardNetDepth()); + + // Shut down + tracker.shutDown(); + + // After shutdown, should return fresh values from client (not cached) + when(client.getVarbitValue(TRAWLING_NET_PORT_VARBIT)).thenReturn(1); + when(client.getVarbitValue(TRAWLING_NET_STARBOARD_VARBIT)).thenReturn(1); + + assertEquals(ShoalDepth.SHALLOW, tracker.getPortNetDepth()); + assertEquals(ShoalDepth.SHALLOW, tracker.getStarboardNetDepth()); + } +} \ No newline at end of file diff --git a/src/test/java/com/duckblade/osrs/sailing/features/trawling/ShoalOverlayTest.java b/src/test/java/com/duckblade/osrs/sailing/features/trawling/ShoalOverlayTest.java new file mode 100644 index 00000000..3f26ef8d --- /dev/null +++ b/src/test/java/com/duckblade/osrs/sailing/features/trawling/ShoalOverlayTest.java @@ -0,0 +1,93 @@ +package com.duckblade.osrs.sailing.features.trawling; + +import com.duckblade.osrs.sailing.SailingConfig; +import com.duckblade.osrs.sailing.features.util.BoatTracker; +import net.runelite.api.Client; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.awt.Color; +import java.lang.reflect.Method; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +/** + * Tests for ShoalOverlay color logic (depth matching disabled) + */ +public class ShoalOverlayTest { + + @Mock + private Client client; + + @Mock + private SailingConfig config; + + @Mock + private ShoalTracker shoalTracker; + + @Mock + private NetDepthTimer netDepthTimer; + + private ShoalOverlay overlay; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + overlay = new ShoalOverlay(client, config, shoalTracker, netDepthTimer); + + // Setup default config color + when(config.trawlingShoalHighlightColour()).thenReturn(Color.CYAN); + } + + /** + * Test that special shoals use green color + */ + @Test + public void testSpecialShoalsUseGreenColor() throws Exception { + int[] specialShoalIds = { + TrawlingData.ShoalObjectID.VIBRANT, + TrawlingData.ShoalObjectID.GLISTENING, + TrawlingData.ShoalObjectID.SHIMMERING + }; + + for (int shoalId : specialShoalIds) { + Color color = getShoalColorViaReflection(shoalId); + assertEquals("Special shoal ID " + shoalId + " should use green color", + Color.GREEN, color); + } + } + + /** + * Test that normal shoals use configured color + */ + @Test + public void testNormalShoalsUseConfiguredColor() throws Exception { + int[] normalShoalIds = { + TrawlingData.ShoalObjectID.BLUEFIN, + TrawlingData.ShoalObjectID.MARLIN, + TrawlingData.ShoalObjectID.HALIBUT, + TrawlingData.ShoalObjectID.YELLOWFIN + }; + + Color testColor = Color.MAGENTA; + when(config.trawlingShoalHighlightColour()).thenReturn(testColor); + + for (int shoalId : normalShoalIds) { + Color color = getShoalColorViaReflection(shoalId); + assertEquals("Normal shoal ID " + shoalId + " should use configured color", + testColor, color); + } + } + + /** + * Helper method to access private getShoalColor method via reflection + */ + private Color getShoalColorViaReflection(int objectId) throws Exception { + Method getShoalColorMethod = ShoalOverlay.class.getDeclaredMethod("getShoalColor", int.class); + getShoalColorMethod.setAccessible(true); + return (Color) getShoalColorMethod.invoke(overlay, objectId); + } +} \ No newline at end of file diff --git a/src/test/java/com/duckblade/osrs/sailing/model/FishingNetTierTest.java b/src/test/java/com/duckblade/osrs/sailing/model/FishingNetTierTest.java new file mode 100644 index 00000000..c89705ad --- /dev/null +++ b/src/test/java/com/duckblade/osrs/sailing/model/FishingNetTierTest.java @@ -0,0 +1,134 @@ +package com.duckblade.osrs.sailing.model; + +import net.runelite.api.gameval.ObjectID; +import org.junit.Assert; +import org.junit.Test; + +public class FishingNetTierTest { + + @Test + public void testFromGameObjectId_ropeNet() { + Assert.assertEquals(FishingNetTier.ROPE, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_ROPE_TRAWLING_NET)); + } + + @Test + public void testFromGameObjectId_ropeNetPort() { + Assert.assertEquals(FishingNetTier.ROPE, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_ROPE_TRAWLING_NET_3X8_PORT)); + } + + @Test + public void testFromGameObjectId_ropeNetStarboard() { + Assert.assertEquals(FishingNetTier.ROPE, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_ROPE_TRAWLING_NET_3X8_STARBOARD)); + } + + @Test + public void testFromGameObjectId_linenNet() { + Assert.assertEquals(FishingNetTier.LINEN, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_LINEN_TRAWLING_NET)); + } + + @Test + public void testFromGameObjectId_linenNetPort() { + Assert.assertEquals(FishingNetTier.LINEN, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_LINEN_TRAWLING_NET_3X8_PORT)); + } + + @Test + public void testFromGameObjectId_linenNetStarboard() { + Assert.assertEquals(FishingNetTier.LINEN, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_LINEN_TRAWLING_NET_3X8_STARBOARD)); + } + + @Test + public void testFromGameObjectId_hempNet() { + Assert.assertEquals(FishingNetTier.HEMP, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_HEMP_TRAWLING_NET)); + } + + @Test + public void testFromGameObjectId_hempNetPort() { + Assert.assertEquals(FishingNetTier.HEMP, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_HEMP_TRAWLING_NET_3X8_PORT)); + } + + @Test + public void testFromGameObjectId_hempNetStarboard() { + Assert.assertEquals(FishingNetTier.HEMP, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_HEMP_TRAWLING_NET_3X8_STARBOARD)); + } + + @Test + public void testFromGameObjectId_cottonNet() { + Assert.assertEquals(FishingNetTier.COTTON, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_COTTON_TRAWLING_NET)); + } + + @Test + public void testFromGameObjectId_cottonNetPort() { + Assert.assertEquals(FishingNetTier.COTTON, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_COTTON_TRAWLING_NET_3X8_PORT)); + } + + @Test + public void testFromGameObjectId_cottonNetStarboard() { + Assert.assertEquals(FishingNetTier.COTTON, + FishingNetTier.fromGameObjectId(ObjectID.SAILING_COTTON_TRAWLING_NET_3X8_STARBOARD)); + } + + @Test + public void testFromGameObjectId_invalidId_returnsNull() { + Assert.assertNull(FishingNetTier.fromGameObjectId(12345)); + } + + @Test + public void testFromGameObjectId_allTiers() { + // Test that all tiers can be found + Assert.assertNotNull(FishingNetTier.fromGameObjectId(ObjectID.SAILING_ROPE_TRAWLING_NET)); + Assert.assertNotNull(FishingNetTier.fromGameObjectId(ObjectID.SAILING_LINEN_TRAWLING_NET)); + Assert.assertNotNull(FishingNetTier.fromGameObjectId(ObjectID.SAILING_HEMP_TRAWLING_NET)); + Assert.assertNotNull(FishingNetTier.fromGameObjectId(ObjectID.SAILING_COTTON_TRAWLING_NET)); + } + + @Test + public void testGetCapacity_allTiers() { + // Currently all tiers return 125 + // This test documents the current behavior + Assert.assertEquals(125, FishingNetTier.ROPE.getCapacity()); + Assert.assertEquals(125, FishingNetTier.LINEN.getCapacity()); + Assert.assertEquals(125, FishingNetTier.HEMP.getCapacity()); + Assert.assertEquals(125, FishingNetTier.COTTON.getCapacity()); + } + + @Test + public void testGetGameObjectIds_ropeHasThreeIds() { + Assert.assertEquals(3, FishingNetTier.ROPE.getGameObjectIds().length); + } + + @Test + public void testGetGameObjectIds_linenHasThreeIds() { + Assert.assertEquals(3, FishingNetTier.LINEN.getGameObjectIds().length); + } + + @Test + public void testGetGameObjectIds_hempHasThreeIds() { + Assert.assertEquals(3, FishingNetTier.HEMP.getGameObjectIds().length); + } + + @Test + public void testGetGameObjectIds_cottonHasThreeIds() { + Assert.assertEquals(3, FishingNetTier.COTTON.getGameObjectIds().length); + } + + @Test + public void testAllTiersExist() { + FishingNetTier[] tiers = FishingNetTier.values(); + Assert.assertEquals(4, tiers.length); + Assert.assertEquals(FishingNetTier.ROPE, tiers[0]); + Assert.assertEquals(FishingNetTier.LINEN, tiers[1]); + Assert.assertEquals(FishingNetTier.HEMP, tiers[2]); + Assert.assertEquals(FishingNetTier.COTTON, tiers[3]); + } +}