From bb55defb5055f709f059e318b11bbfa3b438472c Mon Sep 17 00:00:00 2001 From: sdburns1998 <49877861+sdburns1998@users.noreply.github.com> Date: Mon, 20 May 2019 18:29:46 +0200 Subject: [PATCH] Ground items (#323) * retrieve ItemLayer height in render rather than on spawn * Ground item timers * Loot notifications --- .../plugins/grounditems/GroundItem.java | 5 ++ .../grounditems/GroundItemsConfig.java | 80 ++++++++++++++--- .../grounditems/GroundItemsOverlay.java | 84 ++++++++++++++++++ .../grounditems/GroundItemsPlugin.java | 85 ++++++++++++++++++- .../grounditems/config/TimerDisplayMode.java | 45 ++++++++++ 5 files changed, 284 insertions(+), 15 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/grounditems/config/TimerDisplayMode.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java index 883d3e9df2..ce66dd8a8f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java @@ -24,6 +24,7 @@ */ package net.runelite.client.plugins.grounditems; +import java.time.Instant; import lombok.Builder; import lombok.Data; import lombok.Value; @@ -45,6 +46,10 @@ class GroundItem private int offset; private boolean tradeable; private boolean isMine; + private int durationMillis; + private boolean isAlwaysPrivate; + private boolean isOwnedByPlayer; + private Instant droppedInstant; int getHaPrice() { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java index c722b202f0..ffacca5af2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java @@ -32,6 +32,7 @@ import net.runelite.client.config.ConfigItem; import net.runelite.client.plugins.grounditems.config.ItemHighlightMode; import net.runelite.client.plugins.grounditems.config.MenuHighlightMode; import net.runelite.client.plugins.grounditems.config.PriceDisplayMode; +import net.runelite.client.plugins.grounditems.config.TimerDisplayMode; import net.runelite.client.plugins.grounditems.config.ValueCalculationMode; @ConfigGroup("grounditems") @@ -260,11 +261,22 @@ public interface GroundItemsConfig extends Config return 20000; } + @ConfigItem( + keyName = "notifyLowValueDrops", + name = "Notify for low value drops", + description = "Configures whether or not to notify for drops of low value", + position = 19 + ) + default boolean notifyLowValueDrops() + { + return false; + } + @ConfigItem( keyName = "mediumValueColor", name = "Medium value items color", description = "Configures the color for medium value items", - position = 19 + position = 20 ) default Color mediumValueColor() { @@ -275,18 +287,29 @@ public interface GroundItemsConfig extends Config keyName = "mediumValuePrice", name = "Medium value price", description = "Configures the start price for medium value items", - position = 20 + position = 21 ) default int mediumValuePrice() { return 100000; } + @ConfigItem( + keyName = "notifyMediumValueDrops", + name = "Notify for medium value drops", + description = "Configures whether or not to notify for drops of medium value", + position = 22 + ) + default boolean notifyMediumValueDrops() + { + return false; + } + @ConfigItem( keyName = "highValueColor", name = "High value items color", description = "Configures the color for high value items", - position = 21 + position = 23 ) default Color highValueColor() { @@ -297,18 +320,29 @@ public interface GroundItemsConfig extends Config keyName = "highValuePrice", name = "High value price", description = "Configures the start price for high value items", - position = 22 + position = 24 ) default int highValuePrice() { return 1000000; } + @ConfigItem( + keyName = "notifyHighValueDrops", + name = "Notify for high value drops", + description = "Configures whether or not to notify for drops of high value", + position = 25 + ) + default boolean notifyHighValueDrops() + { + return false; + } + @ConfigItem( keyName = "insaneValueColor", name = "Insane value items color", description = "Configures the color for insane value items", - position = 23 + position = 26 ) default Color insaneValueColor() { @@ -319,18 +353,29 @@ public interface GroundItemsConfig extends Config keyName = "insaneValuePrice", name = "Insane value price", description = "Configures the start price for insane value items", - position = 24 + position = 27 ) default int insaneValuePrice() { return 10000000; } + @ConfigItem( + keyName = "notifyInsaneValueDrops", + name = "Notify for insane value drops", + description = "Configures whether or not to notify for drops of insane value", + position = 28 + ) + default boolean notifyInsaneValueDrops() + { + return false; + } + @ConfigItem( keyName = "onlyShowLoot", name = "Only show loot", description = "Only shows drops from NPCs and players", - position = 25 + position = 29 ) default boolean onlyShowLoot() { @@ -341,7 +386,7 @@ public interface GroundItemsConfig extends Config keyName = "doubleTapDelay", name = "Delay for double-tap ALT to hide", description = "Decrease this number if you accidentally hide ground items often. (0 = Disabled)", - position = 26 + position = 30 ) default int doubleTapDelay() { @@ -352,7 +397,7 @@ public interface GroundItemsConfig extends Config keyName = "collapseEntries", name = "Collapse ground item menu entries", description = "Collapses ground item menu entries together and appends count", - position = 27 + position = 31 ) default boolean collapseEntries() { @@ -360,10 +405,10 @@ public interface GroundItemsConfig extends Config } @ConfigItem( - position = 27, keyName = "removeIgnored", name = "Hide Ignored", - description = "Remove take option for items that are on the hidden items list." + description = "Remove take option for items that are on the hidden items list.", + position = 32 ) default boolean removeIgnored() { @@ -374,10 +419,21 @@ public interface GroundItemsConfig extends Config keyName = "toggleOutline", name = "Text Outline", description = "Use an outline around text instead of a text shadow", - position = 29 + position = 33 ) default boolean toggleOutline() { return false; } + + @ConfigItem( + keyName = "showGroundItemDuration", + name = "Show time remaining", + description = "Turn on a countdown timer to show how long an item will remain on the ground", + position = 34 + ) + default TimerDisplayMode showGroundItemDuration() + { + return TimerDisplayMode.HOTKEY_PRESSED; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java index 4d7e2601cd..024d9455b1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java @@ -31,6 +31,8 @@ import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Polygon; import java.awt.Rectangle; +import java.time.Duration; +import java.time.Instant; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collection; @@ -41,15 +43,18 @@ import net.runelite.api.Client; import net.runelite.api.Perspective; import net.runelite.api.Player; import net.runelite.api.Point; +import net.runelite.api.Tile; import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; import static net.runelite.client.plugins.grounditems.config.ItemHighlightMode.MENU; import net.runelite.client.plugins.grounditems.config.PriceDisplayMode; +import net.runelite.client.plugins.grounditems.config.TimerDisplayMode; 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 net.runelite.client.ui.overlay.components.BackgroundComponent; +import net.runelite.client.ui.overlay.components.ProgressPieComponent; import net.runelite.client.ui.overlay.components.TextComponent; import net.runelite.client.util.StackFormatter; @@ -67,12 +72,20 @@ public class GroundItemsOverlay extends Overlay // Size of the hidden/highlight boxes private static final int RECTANGLE_SIZE = 8; + private static final int TIMER_OVERLAY_DIAMETER = 10; + private static final int PUBLIC_ITEM_DURATION_MILLIS = 60000; + private static final float WARNING_THRESHOLD = 0.25f; + private static final Color PUBLIC_TIMER_COLOR = Color.YELLOW; + private static final Color PRIVATE_TIMER_COLOR = Color.GREEN; + private static final Color PUBLIC_WARNING_TIMER_COLOR = Color.RED; + private final Client client; private final GroundItemsPlugin plugin; private final GroundItemsConfig config; private final StringBuilder itemStringBuilder = new StringBuilder(); private final BackgroundComponent backgroundComponent = new BackgroundComponent(); private final TextComponent textComponent = new TextComponent(); + private final ProgressPieComponent progressPieComponent = new ProgressPieComponent(); private final Map offsetMap = new HashMap<>(); @Inject @@ -259,6 +272,13 @@ public class GroundItemsOverlay extends Overlay final String itemString = itemStringBuilder.toString(); itemStringBuilder.setLength(0); + if (item.getHeight() == -1) + { + final Tile[][][] sceneTiles = client.getScene().getTiles(); + final Tile itemTile = sceneTiles[client.getPlane()][groundPoint.getSceneX()][groundPoint.getSceneY()]; + item.setHeight(itemTile.getItemLayer().getHeight()); + } + final Point textPoint = Perspective.getCanvasTextLocation(client, graphics, groundPoint, @@ -333,6 +353,12 @@ public class GroundItemsOverlay extends Overlay drawRectangle(graphics, itemHighlightBox, topItem && mouseInHighlightBox ? Color.GREEN : color, highlighted != null, false); } + if (config.showGroundItemDuration() == TimerDisplayMode.ALWAYS + || (config.showGroundItemDuration() == TimerDisplayMode.HOTKEY_PRESSED && plugin.isHotKeyPressed())) + { + drawTimerOverlay(graphics, new java.awt.Point(textX, textY), item); + } + if (config.toggleOutline()) { graphics.setColor(Color.BLACK); @@ -388,4 +414,62 @@ public class GroundItemsOverlay extends Overlay } + private void drawTimerOverlay(Graphics2D graphics, java.awt.Point location, GroundItem item) + { + progressPieComponent.setDiameter(TIMER_OVERLAY_DIAMETER); + + int x = (int) location.getX() - TIMER_OVERLAY_DIAMETER; + int y = (int) location.getY() - TIMER_OVERLAY_DIAMETER / 2; + + progressPieComponent.setPosition(new Point(x, y)); + + double millisOnGround = Duration.between(item.getDroppedInstant(), Instant.now()).toMillis(); + boolean isPubliclyVisible = !item.isAlwaysPrivate() && millisOnGround > item.getDurationMillis(); + double timeLeftRelative; + Color fillColor; + + if (isPubliclyVisible || !item.isOwnedByPlayer()) + { + if (item.isOwnedByPlayer()) + { + timeLeftRelative = getTimeLeftRelative(millisOnGround - PUBLIC_ITEM_DURATION_MILLIS, PUBLIC_ITEM_DURATION_MILLIS); + + } + else + { + timeLeftRelative = getTimeLeftRelative(millisOnGround, PUBLIC_ITEM_DURATION_MILLIS); + } + + if (timeLeftRelative < WARNING_THRESHOLD) + { + fillColor = PUBLIC_WARNING_TIMER_COLOR; + } + else + { + fillColor = PUBLIC_TIMER_COLOR; + } + } + else + { + timeLeftRelative = getTimeLeftRelative(millisOnGround, item.getDurationMillis()); + fillColor = PRIVATE_TIMER_COLOR; + + } + + // don't draw timer for any permanently spawned items or broken edge cases + if (timeLeftRelative > 1 || timeLeftRelative < 0) + { + return; + } + + progressPieComponent.setFill(fillColor); + progressPieComponent.setBorderColor(fillColor); + progressPieComponent.setProgress(timeLeftRelative); + progressPieComponent.render(graphics); + } + + private double getTimeLeftRelative(double millisOnGround, int duration) + { + return (duration - millisOnGround) / duration; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java index 35ee979085..3b8a06ec13 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java @@ -31,6 +31,7 @@ import com.google.inject.Provides; import java.awt.Color; import java.awt.Rectangle; import static java.lang.Boolean.TRUE; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -99,6 +100,15 @@ public class GroundItemsPlugin extends Plugin private static final float HIGH_ALCHEMY_CONSTANT = 0.6f; // ItemID for coins private static final int COINS = ItemID.COINS_995; + + // items stay on the ground for 30 mins in an instance + private static final int INSTANCE_DURATION_MILLIS = 45 * 60 * 1000; + //untradeables stay on the ground for 150 seconds (http://oldschoolrunescape.wikia.com/wiki/Item#Dropping_and_Destroying) + private static final int UNTRADEABLE_DURATION_MILLIS = 150 * 1000; + //items stay on the ground for 1 hour after death + private static final int DEATH_DURATION_MILLIS = 60 * 60 * 1000; + private static final int NORMAL_DURATION_MILLIS = 60 * 1000; + // Ground item menu options private static final int FIRST_OPTION = MenuAction.GROUND_ITEM_FIRST_OPTION.getId(); private static final int SECOND_OPTION = MenuAction.GROUND_ITEM_SECOND_OPTION.getId(); @@ -280,8 +290,19 @@ public class GroundItemsPlugin extends Plugin @Subscribe public void onNpcLootReceived(NpcLootReceived npcLootReceived) { + npcLootReceived.getItems().forEach(item -> + { + GroundItem.GroundItemKey groundItemKey = new GroundItem.GroundItemKey(item.getId(), npcLootReceived.getNpc().getWorldLocation()); + if (collectedGroundItems.containsKey(groundItemKey)) + { + collectedGroundItems.get(groundItemKey).setOwnedByPlayer(true); + } + } + ); + Collection items = npcLootReceived.getItems(); lootReceived(items); + lootNotifier(items); } @Subscribe @@ -289,6 +310,47 @@ public class GroundItemsPlugin extends Plugin { Collection items = playerLootReceived.getItems(); lootReceived(items); + lootNotifier(items); + } + + private void lootNotifier(Collection items) + { + ItemComposition composition; + for (ItemStack is : items) + { + composition = itemManager.getItemComposition(is.getId()); + Color itemColor = getHighlighted(composition.getName(), itemManager.getItemPrice(is.getId()) * is.getQuantity(), Math.round(composition.getPrice() * HIGH_ALCHEMY_CONSTANT) * is.getQuantity()); + if (itemColor != null) + { + if (config.notifyHighlightedDrops() && itemColor.equals(config.highlightedColor())) + { + sendLootNotification(composition.getName(), "highlighted"); + } + else if (config.notifyLowValueDrops() && itemColor.equals(config.lowValueColor())) + { + sendLootNotification(composition.getName(), "low value"); + } + else if (config.notifyMediumValueDrops() && itemColor.equals(config.mediumValueColor())) + { + sendLootNotification(composition.getName(), "medium value"); + } + else if (config.notifyHighValueDrops() && itemColor.equals(config.highValueColor())) + { + sendLootNotification(composition.getName(), "high value"); + } + else if (config.notifyInsaneValueDrops() && itemColor.equals(config.insaneValueColor())) + { + sendLootNotification(composition.getName(), "insane value"); + } + } + } + } + + private void sendLootNotification(String itemName, String message) + { + String notification = "[" + client.getLocalPlayer().getName() + "] " + + "Received a " + message + " item: " + itemName; + notifier.notify(notification); } @Subscribe @@ -370,6 +432,21 @@ public class GroundItemsPlugin extends Plugin final ItemComposition itemComposition = itemManager.getItemComposition(itemId); final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemId; final int alchPrice = Math.round(itemComposition.getPrice() * HIGH_ALCHEMY_CONSTANT); + int durationMillis; + if (client.isInInstancedRegion()) + { + durationMillis = INSTANCE_DURATION_MILLIS; + } + else if (!itemComposition.isTradeable() && realItemId != COINS) + { + durationMillis = UNTRADEABLE_DURATION_MILLIS; + } + else + { + durationMillis = NORMAL_DURATION_MILLIS; + } + + WorldPoint playerLocation = client.getLocalPlayer().getWorldLocation(); final GroundItem groundItem = GroundItem.builder() .id(itemId) @@ -378,8 +455,12 @@ public class GroundItemsPlugin extends Plugin .quantity(item.getQuantity()) .name(itemComposition.getName()) .haPrice(alchPrice) - .height(tile.getItemLayer().getHeight()) + .height(-1) .tradeable(itemComposition.isTradeable()) + .droppedInstant(Instant.now()) + .durationMillis(durationMillis) + .isAlwaysPrivate(client.isInInstancedRegion() || (!itemComposition.isTradeable() && realItemId != COINS)) + .isOwnedByPlayer(tile.getWorldLocation().equals(playerLocation)) .build(); @@ -539,8 +620,6 @@ public class GroundItemsPlugin extends Plugin { return entries; } - - } void updateList(String item, boolean hiddenList) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/config/TimerDisplayMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/config/TimerDisplayMode.java new file mode 100644 index 0000000000..3a6773078e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/config/TimerDisplayMode.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018 Matthew Smith + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.grounditems.config; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TimerDisplayMode +{ + ALWAYS("Always"), + HOTKEY_PRESSED("When Hotkey Pressed"), + NEVER("Never"); + + private final String name; + + @Override + public String toString() + { + return name; + } +} \ No newline at end of file