diff --git a/runelite-api/src/main/java/net/runelite/api/AnimationID.java b/runelite-api/src/main/java/net/runelite/api/AnimationID.java index c3e0ba8e1b..891d1f050d 100644 --- a/runelite-api/src/main/java/net/runelite/api/AnimationID.java +++ b/runelite-api/src/main/java/net/runelite/api/AnimationID.java @@ -157,4 +157,13 @@ public final class AnimationID // Battlestaff Crafting public static final int CRAFTING_BATTLESTAVES = 7531; + + // Death Animations + public static final int CAVE_KRAKEN_DEATH = 3993; + public static final int WIZARD_DEATH = 2553; + public static final int GARGOYLE_DEATH = 1520; + public static final int MARBLE_GARGOYLE_DEATH = 7813; + public static final int LIZARD_DEATH = 2778; + public static final int ROCKSLUG_DEATH = 1568; + public static final int ZYGOMITE_DEATH = 3327; } diff --git a/runelite-api/src/main/java/net/runelite/api/InventoryID.java b/runelite-api/src/main/java/net/runelite/api/InventoryID.java index 7c31d9d819..ad97bc5c5d 100644 --- a/runelite-api/src/main/java/net/runelite/api/InventoryID.java +++ b/runelite-api/src/main/java/net/runelite/api/InventoryID.java @@ -48,7 +48,15 @@ public enum InventoryID /** * Barrows reward chest inventory. */ - BARROWS_REWARD(141); + BARROWS_REWARD(141), + /** + * Chambers of Xeric chest inventory. + */ + CHAMBERS_OF_XERIC_CHEST(581), + /** + * Theater of Blood reward chest inventory (Raids 2) + */ + THEATRE_OF_BLOOD_CHEST(10); private final int id; diff --git a/runelite-api/src/main/java/net/runelite/api/Varbits.java b/runelite-api/src/main/java/net/runelite/api/Varbits.java index f9a378dbdc..3e452844c1 100644 --- a/runelite-api/src/main/java/net/runelite/api/Varbits.java +++ b/runelite-api/src/main/java/net/runelite/api/Varbits.java @@ -259,6 +259,11 @@ public enum Varbits PERSONAL_POINTS(5422), RAID_PARTY_SIZE(5424), + /** + * Theatre of Blood 1=In Party, 2=Inside/Spectator, 3=Dead Spectating + */ + THEATRE_OF_BLOOD(6440), + /** * Nightmare Zone */ diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java index a0e7b3dced..e52486139e 100644 --- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java +++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java @@ -107,6 +107,7 @@ public class WidgetID public static final int VARROCK_MUSEUM_QUIZ_GROUP_ID = 533; public static final int KILL_LOGS_GROUP_ID = 549; public static final int DIARY_QUEST_GROUP_ID = 275; + public static final int THEATRE_OF_BLOOD_GROUP_ID = 23; static class WorldMap { diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index 930c938019..bbc0039f98 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -52,6 +52,7 @@ import net.runelite.client.config.ConfigManager; import net.runelite.client.discord.DiscordService; import net.runelite.client.game.ClanManager; import net.runelite.client.game.ItemManager; +import net.runelite.client.game.LootManager; import net.runelite.client.menus.MenuManager; import net.runelite.client.plugins.PluginManager; import net.runelite.client.rs.ClientUpdateCheckMode; @@ -137,6 +138,9 @@ public class RuneLite @Inject private Provider worldMapOverlay; + @Inject + private Provider lootManager; + @Inject @Nullable private Client client; @@ -273,6 +277,7 @@ public class RuneLite eventBus.register(menuManager.get()); eventBus.register(chatMessageManager.get()); eventBus.register(commandManager.get()); + eventBus.register(lootManager.get()); // Add core overlays WidgetOverlay.createOverlays(client).forEach(overlayManager::add); diff --git a/runelite-client/src/main/java/net/runelite/client/events/NpcLootReceived.java b/runelite-client/src/main/java/net/runelite/client/events/NpcLootReceived.java new file mode 100644 index 0000000000..e3ef8a03da --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/events/NpcLootReceived.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018, Adam + * 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.events; + +import java.util.Collection; +import lombok.Value; +import net.runelite.api.NPC; +import net.runelite.client.game.ItemStack; + +@Value +public class NpcLootReceived +{ + private final NPC npc; + private final Collection items; +} diff --git a/runelite-client/src/main/java/net/runelite/client/events/PlayerLootReceived.java b/runelite-client/src/main/java/net/runelite/client/events/PlayerLootReceived.java new file mode 100644 index 0000000000..56eb722a83 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/events/PlayerLootReceived.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018, Adam + * 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.events; + +import java.util.Collection; +import lombok.Value; +import net.runelite.api.Player; +import net.runelite.client.game.ItemStack; + +@Value +public class PlayerLootReceived +{ + private final Player player; + private final Collection items; +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/game/ItemStack.java b/runelite-client/src/main/java/net/runelite/client/game/ItemStack.java new file mode 100644 index 0000000000..bc455e2cf8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/game/ItemStack.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018, Adam + * 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.game; + +import lombok.Value; + +@Value +public class ItemStack +{ + private final int id; + private final int quantity; +} diff --git a/runelite-client/src/main/java/net/runelite/client/game/LootManager.java b/runelite-client/src/main/java/net/runelite/client/game/LootManager.java new file mode 100644 index 0000000000..9443c3eff9 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/game/LootManager.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2018, Adam + * 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.game; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ListMultimap; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.AnimationID; +import net.runelite.api.Client; +import net.runelite.api.Item; +import net.runelite.api.ItemID; +import net.runelite.api.NPC; +import net.runelite.api.NpcID; +import net.runelite.api.Player; +import net.runelite.api.Tile; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.AnimationChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.ItemQuantityChanged; +import net.runelite.api.events.ItemSpawned; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.PlayerDespawned; +import net.runelite.client.events.NpcLootReceived; +import net.runelite.client.events.PlayerLootReceived; + +@Singleton +@Slf4j +public class LootManager +{ + private static final Map NPC_DEATH_ANIMATIONS = ImmutableMap.of( + NpcID.CAVE_KRAKEN, AnimationID.CAVE_KRAKEN_DEATH, + NpcID.AIR_WIZARD, AnimationID.WIZARD_DEATH, + NpcID.WATER_WIZARD, AnimationID.WIZARD_DEATH, + NpcID.EARTH_WIZARD, AnimationID.WIZARD_DEATH, + NpcID.FIRE_WIZARD, AnimationID.WIZARD_DEATH + ); + + private final EventBus eventBus; + private final Client client; + private final ListMultimap itemSpawns = ArrayListMultimap.create(); + private WorldPoint playerLocationLastTick; + private WorldPoint krakenPlayerLocation; + + @Inject + private LootManager(EventBus eventBus, Client client) + { + this.eventBus = eventBus; + this.client = client; + } + + @Subscribe + public void onNpcDespawned(NpcDespawned npcDespawned) + { + final NPC npc = npcDespawned.getNpc(); + if (!npc.isDead()) + { + int id = npc.getId(); + switch (id) + { + case NpcID.GARGOYLE: + case NpcID.GARGOYLE_413: + case NpcID.GARGOYLE_1543: + case NpcID.MARBLE_GARGOYLE: + case NpcID.MARBLE_GARGOYLE_7408: + + case NpcID.ROCKSLUG: + case NpcID.ROCKSLUG_422: + case NpcID.GIANT_ROCKSLUG: + + case NpcID.SMALL_LIZARD: + case NpcID.SMALL_LIZARD_463: + case NpcID.DESERT_LIZARD: + case NpcID.DESERT_LIZARD_460: + case NpcID.DESERT_LIZARD_461: + case NpcID.LIZARD: + + case NpcID.ZYGOMITE: + case NpcID.ZYGOMITE_474: + case NpcID.ANCIENT_ZYGOMITE: + + // these monsters die with >0 hp, so we just look for coincident + // item spawn with despawn + break; + default: + return; + } + } + + processNpcLoot(npc); + } + + @Subscribe + public void onPlayerDespawned(PlayerDespawned playerDespawned) + { + final Player player = playerDespawned.getPlayer(); + final LocalPoint location = LocalPoint.fromWorld(client, player.getWorldLocation()); + if (location == null) + { + return; + } + + final int x = location.getSceneX(); + final int y = location.getSceneY(); + final int packed = x << 8 | y; + final Collection items = itemSpawns.get(packed); + + if (items.isEmpty()) + { + return; + } + + eventBus.post(new PlayerLootReceived(player, items)); + } + + @Subscribe + public void onItemSpawned(ItemSpawned itemSpawned) + { + final Item item = itemSpawned.getItem(); + final Tile tile = itemSpawned.getTile(); + final LocalPoint location = tile.getLocalLocation(); + final int packed = location.getSceneX() << 8 | location.getSceneY(); + itemSpawns.put(packed, new ItemStack(item.getId(), item.getQuantity())); + log.debug("Item spawn {} location {},{}", item.getId(), location); + } + + @Subscribe + public void onItemQuantityChanged(ItemQuantityChanged itemQuantityChanged) + { + final Item item = itemQuantityChanged.getItem(); + final Tile tile = itemQuantityChanged.getTile(); + final LocalPoint location = tile.getLocalLocation(); + final int packed = location.getSceneX() << 8 | location.getSceneY(); + final int diff = itemQuantityChanged.getNewQuantity() - itemQuantityChanged.getOldQuantity(); + + if (diff <= 0) + { + return; + } + + itemSpawns.put(packed, new ItemStack(item.getId(), diff)); + } + + @Subscribe + public void onAnimationChanged(AnimationChanged e) + { + if (!(e.getActor() instanceof NPC)) + { + return; + } + + final NPC npc = (NPC) e.getActor(); + int id = npc.getId(); + + // We only care about certain NPCs + final Integer deathAnim = NPC_DEATH_ANIMATIONS.get(id); + + // Current animation is death animation? + if (deathAnim != null && deathAnim == npc.getAnimation()) + { + if (id == NpcID.CAVE_KRAKEN) + { + // Big Kraken drops loot wherever player is standing when animation starts. + krakenPlayerLocation = client.getLocalPlayer().getWorldLocation(); + } + else + { + // These NPCs drop loot on death animation, which is right now. + processNpcLoot(npc); + } + } + } + + @Subscribe + public void onGameTick(GameTick gameTick) + { + playerLocationLastTick = client.getLocalPlayer().getWorldLocation(); + itemSpawns.clear(); + } + + private void processNpcLoot(NPC npc) + { + final LocalPoint location = LocalPoint.fromWorld(client, getDropLocation(npc, npc.getWorldLocation())); + if (location == null) + { + return; + } + + final int x = location.getSceneX(); + final int y = location.getSceneY(); + final int size = npc.getComposition().getSize(); + + // Some NPCs drop items onto multiple tiles + final List allItems = new ArrayList<>(); + for (int i = 0; i < size; ++i) + { + for (int j = 0; j < size; ++j) + { + final int packed = (x + i) << 8 | (y + j); + final Collection items = itemSpawns.get(packed); + allItems.addAll(items); + } + } + + if (allItems.isEmpty()) + { + return; + } + + eventBus.post(new NpcLootReceived(npc, allItems)); + } + + private WorldPoint getDropLocation(NPC npc, WorldPoint worldLocation) + { + switch (npc.getId()) + { + case NpcID.KRAKEN: + case NpcID.KRAKEN_6640: + case NpcID.KRAKEN_6656: + worldLocation = playerLocationLastTick; + break; + case NpcID.CAVE_KRAKEN: + worldLocation = krakenPlayerLocation; + break; + case NpcID.ZULRAH: // Green + case NpcID.ZULRAH_2043: // Red + case NpcID.ZULRAH_2044: // Blue + for (Map.Entry entry : itemSpawns.entries()) + { + if (entry.getValue().getId() == ItemID.ZULRAHS_SCALES) + { + int packed = entry.getKey(); + int unpackedX = packed >> 8; + int unpackedY = packed & 0xFF; + worldLocation = new WorldPoint(unpackedX, unpackedY, worldLocation.getPlane()); + break; + } + } + break; + case NpcID.VORKATH: + case NpcID.VORKATH_8058: + case NpcID.VORKATH_8059: + case NpcID.VORKATH_8060: + case NpcID.VORKATH_8061: + int x = worldLocation.getX() + 3; + int y = worldLocation.getY() + 3; + if (playerLocationLastTick.getX() < x) + { + x -= 4; + } + else if (playerLocationLastTick.getX() > x) + { + x += 4; + } + if (playerLocationLastTick.getY() < y) + { + y -= 4; + } + else if (playerLocationLastTick.getY() > y) + { + y += 4; + } + worldLocation = new WorldPoint(x, y, worldLocation.getPlane()); + break; + } + + return worldLocation; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerBox.java new file mode 100644 index 0000000000..b56781b999 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerBox.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2018, Psikoi + * Copyright (c) 2018, Tomas Slusny + * 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.loottracker; + +import com.google.common.base.Strings; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.GridLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import javax.swing.border.EmptyBorder; +import lombok.Getter; +import net.runelite.api.ItemID; +import net.runelite.client.game.ItemManager; +import net.runelite.client.game.ItemStack; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import net.runelite.client.util.StackFormatter; +import net.runelite.http.api.item.ItemPrice; + +@Getter +class LootTrackerBox extends JPanel +{ + private static final int ITEMS_PER_ROW = 5; + private final long totalPrice; + + LootTrackerBox(final ItemManager itemManager, final String title, final String subTitle, final ItemStack[] items) + { + setLayout(new BorderLayout(0, 1)); + setBorder(new EmptyBorder(5, 0, 0, 0)); + + final JPanel logTitle = new JPanel(new BorderLayout(5, 0)); + logTitle.setBorder(new EmptyBorder(7, 7, 7, 7)); + logTitle.setBackground(ColorScheme.DARKER_GRAY_COLOR.darker()); + + final JLabel titleLabel = new JLabel(title); + titleLabel.setFont(FontManager.getRunescapeSmallFont()); + titleLabel.setForeground(Color.WHITE); + + logTitle.add(titleLabel, BorderLayout.WEST); + + // If we have subtitle, add it + if (!Strings.isNullOrEmpty(subTitle)) + { + final JLabel subTitleLabel = new JLabel(subTitle); + subTitleLabel.setFont(FontManager.getRunescapeSmallFont()); + subTitleLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + logTitle.add(subTitleLabel, BorderLayout.CENTER); + } + + totalPrice = calculatePrice(itemManager, items); + + if (totalPrice > 0) + { + final JLabel priceLabel = new JLabel(StackFormatter.quantityToStackSize(totalPrice) + " gp"); + priceLabel.setFont(FontManager.getRunescapeSmallFont()); + priceLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + logTitle.add(priceLabel, BorderLayout.EAST); + } + + // Calculates how many rows need to be display to fit all items + final int rowSize = ((items.length % ITEMS_PER_ROW == 0) ? 0 : 1) + items.length / ITEMS_PER_ROW; + final JPanel itemContainer = new JPanel(new GridLayout(rowSize, ITEMS_PER_ROW, 1, 1)); + + for (int i = 0; i < rowSize * ITEMS_PER_ROW; i++) + { + final JPanel slotContainer = new JPanel(); + slotContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + if (i < items.length) + { + final ItemStack item = items[i]; + final JLabel imageLabel = new JLabel(); + imageLabel.setToolTipText(buildToolTip(itemManager, item)); + imageLabel.setVerticalAlignment(SwingConstants.CENTER); + imageLabel.setHorizontalAlignment(SwingConstants.CENTER); + itemManager.getImage(item.getId(), item.getQuantity(), item.getQuantity() > 1).addTo(imageLabel); + slotContainer.add(imageLabel); + } + + itemContainer.add(slotContainer); + } + + add(logTitle, BorderLayout.NORTH); + add(itemContainer, BorderLayout.CENTER); + } + + private String buildToolTip(ItemManager itemManager, ItemStack item) + { + final String name = itemManager.getItemComposition(item.getId()).getName(); + final int quantity = item.getQuantity(); + final long price = calculatePrice(itemManager, new ItemStack[]{item}); + + return name + " x " + quantity + " (" + price + ")"; + } + + private static long calculatePrice(final ItemManager itemManager, final ItemStack[] itemStacks) + { + long total = 0; + for (ItemStack itemStack : itemStacks) + { + ItemPrice itemPrice = itemManager.getItemPrice(itemStack.getId()); + if (itemPrice != null) + { + total += (long) itemPrice.getPrice() * itemStack.getQuantity(); + } + else if (itemStack.getId() == ItemID.COINS_995) + { + total += itemStack.getQuantity(); + } + } + return total; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPanel.java new file mode 100644 index 0000000000..8ae2d03735 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPanel.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2018, Psikoi + * Copyright (c) 2018, Tomas Slusny + * 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.loottracker; + +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.image.BufferedImage; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.border.EmptyBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.game.ItemManager; +import net.runelite.client.game.ItemStack; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.PluginErrorPanel; +import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.StackFormatter; + +@Slf4j +class LootTrackerPanel extends PluginPanel +{ + private static final String HTML_LABEL_TEMPLATE = + "%s%s"; + + // When there is no loot, display this + private final PluginErrorPanel errorPanel = new PluginErrorPanel(); + + // Handle loot logs + private final JPanel logsContainer = new JPanel(); + + // Handle overall session data + private final JPanel overallPanel = new JPanel(); + private final JLabel overallKillsLabel = new JLabel(); + private final JLabel overallGpLabel = new JLabel(); + private final JLabel overallIcon = new JLabel(); + private final ItemManager itemManager; + private int overallKills; + private int overallGp; + + LootTrackerPanel(final ItemManager itemManager) + { + this.itemManager = itemManager; + setBorder(new EmptyBorder(6, 6, 6, 6)); + setBackground(ColorScheme.DARK_GRAY_COLOR); + setLayout(new BorderLayout()); + + // Create layout panel for wrapping + final JPanel layoutPanel = new JPanel(); + layoutPanel.setLayout(new BoxLayout(layoutPanel, BoxLayout.Y_AXIS)); + add(layoutPanel, BorderLayout.NORTH); + + // Create panel that will contain overall data + overallPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + overallPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + overallPanel.setLayout(new BorderLayout()); + overallPanel.setVisible(false); + + // Add icon and contents + final JPanel overallInfo = new JPanel(); + overallInfo.setBackground(ColorScheme.DARKER_GRAY_COLOR); + overallInfo.setLayout(new GridLayout(2, 1)); + overallInfo.setBorder(new EmptyBorder(0, 10, 0, 0)); + overallKillsLabel.setFont(FontManager.getRunescapeSmallFont()); + overallGpLabel.setFont(FontManager.getRunescapeSmallFont()); + overallInfo.add(overallKillsLabel); + overallInfo.add(overallGpLabel); + overallPanel.add(overallIcon, BorderLayout.WEST); + overallPanel.add(overallInfo, BorderLayout.CENTER); + + // Create reset all menu + final JMenuItem reset = new JMenuItem("Reset All"); + reset.addActionListener(e -> + { + overallKills = 0; + overallGp = 0; + updateOverall(); + logsContainer.removeAll(); + logsContainer.repaint(); + }); + + // Create popup menu + final JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); + popupMenu.add(reset); + overallPanel.setComponentPopupMenu(popupMenu); + + // Create loot logs wrapper + logsContainer.setLayout(new BoxLayout(logsContainer, BoxLayout.Y_AXIS)); + layoutPanel.add(overallPanel); + layoutPanel.add(logsContainer); + + // Add error pane + errorPanel.setContent("Loot trackers", "You have not killed anything yet."); + add(errorPanel); + } + + void loadHeaderIcon(BufferedImage img) + { + overallIcon.setIcon(new ImageIcon(img)); + } + + private static String htmlLabel(String key, long value) + { + final String valueStr = StackFormatter.quantityToStackSize(value); + return String.format(HTML_LABEL_TEMPLATE, ColorUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR), key, valueStr); + } + + void addLog(final String eventName, final int actorLevel, ItemStack[] items) + { + // Remove error and show overall + remove(errorPanel); + overallPanel.setVisible(true); + + // Create box + final String subTitle = actorLevel > -1 ? "(lvl-" + actorLevel + ")" : ""; + final LootTrackerBox box = new LootTrackerBox(itemManager, eventName, subTitle, items); + logsContainer.add(box, 0); + logsContainer.repaint(); + + // Update overall + overallGp += box.getTotalPrice(); + overallKills += 1; + updateOverall(); + + // Create reset menu + final JMenuItem reset = new JMenuItem("Reset"); + reset.addActionListener(e -> + { + overallGp -= box.getTotalPrice(); + overallKills -= 1; + updateOverall(); + logsContainer.remove(box); + logsContainer.repaint(); + }); + + // Create popup menu + final JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); + popupMenu.add(reset); + box.setComponentPopupMenu(popupMenu); + } + + private void updateOverall() + { + overallKillsLabel.setText(htmlLabel("Total count: ", overallKills)); + overallGpLabel.setText(htmlLabel("Total value: ", overallGp)); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java new file mode 100644 index 0000000000..7773587231 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2018, Psikoi + * Copyright (c) 2018, Adam + * 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.loottracker; + +import com.google.common.eventbus.Subscribe; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.imageio.ImageIO; +import javax.inject.Inject; +import javax.swing.SwingUtilities; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.InventoryID; +import net.runelite.api.ItemContainer; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.SpriteID; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.WidgetLoaded; +import net.runelite.api.widgets.WidgetID; +import net.runelite.client.events.NpcLootReceived; +import net.runelite.client.events.PlayerLootReceived; +import net.runelite.client.game.ItemManager; +import net.runelite.client.game.ItemStack; +import net.runelite.client.game.SpriteManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.util.Text; + +@PluginDescriptor( + name = "Loot Tracker", + description = "Tracks loot from monsters and minigames", + tags = {"drops"}, + enabledByDefault = false +) +@Slf4j +public class LootTrackerPlugin extends Plugin +{ + // Activity/Event loot handling + private static final Pattern CLUE_SCROLL_PATTERN = Pattern.compile("You have completed [0-9]+ ([a-z]+) Treasure Trails."); + + @Inject + private ClientToolbar clientToolbar; + + @Inject + private ItemManager itemManager; + + @Inject + private SpriteManager spriteManager; + + @Inject + private Client client; + + private LootTrackerPanel panel; + private NavigationButton navButton; + private String eventType; + + private static Collection stack(Collection items) + { + final List list = new ArrayList<>(); + + for (final ItemStack item : items) + { + int quantity = 0; + for (final ItemStack i : list) + { + if (i.getId() == item.getId()) + { + quantity = i.getQuantity(); + list.remove(i); + break; + } + } + if (quantity > 0) + { + list.add(new ItemStack(item.getId(), item.getQuantity() + quantity)); + } + else + { + list.add(item); + } + } + + return list; + } + + @Override + protected void startUp() throws Exception + { + panel = new LootTrackerPanel(itemManager); + spriteManager.getSpriteAsync(SpriteID.UNUSED_TAB_INVENTORY, 0, panel::loadHeaderIcon); + + final BufferedImage icon; + synchronized (ImageIO.class) + { + icon = ImageIO.read(LootTrackerPanel.class.getResourceAsStream("panel_icon.png")); + } + + navButton = NavigationButton.builder() + .tooltip("Loot Tracker") + .icon(icon) + .priority(5) + .panel(panel) + .build(); + + clientToolbar.addNavigation(navButton); + } + + @Override + protected void shutDown() + { + clientToolbar.removeNavigation(navButton); + } + + @Subscribe + public void onNpcLootReceived(final NpcLootReceived npcLootReceived) + { + final NPC npc = npcLootReceived.getNpc(); + final Collection items = npcLootReceived.getItems(); + final String name = npc.getName(); + final int combat = npc.getCombatLevel(); + final Collection stackedItems = stack(items); + SwingUtilities.invokeLater(() -> panel.addLog(name, combat, stackedItems.toArray(new ItemStack[0]))); + } + + @Subscribe + public void onPlayerLootReceived(final PlayerLootReceived playerLootReceived) + { + final Player player = playerLootReceived.getPlayer(); + final Collection items = playerLootReceived.getItems(); + final String name = player.getName(); + final int combat = player.getCombatLevel(); + final Collection stackedItems = stack(items); + SwingUtilities.invokeLater(() -> panel.addLog(name, combat, stackedItems.toArray(new ItemStack[0]))); + } + + @Subscribe + public void onWidgetLoaded(WidgetLoaded event) + { + final ItemContainer container; + switch (event.getGroupId()) + { + case (WidgetID.BARROWS_REWARD_GROUP_ID): + eventType = "Barrows"; + container = client.getItemContainer(InventoryID.BARROWS_REWARD); + break; + case (WidgetID.CHAMBERS_OF_XERIC_REWARD_GROUP_ID): + eventType = "Chambers of Xeric"; + container = client.getItemContainer(InventoryID.CHAMBERS_OF_XERIC_CHEST); + break; + case (WidgetID.THEATRE_OF_BLOOD_GROUP_ID): + eventType = "Theatre of Blood"; + container = client.getItemContainer(InventoryID.THEATRE_OF_BLOOD_CHEST); + break; + case (WidgetID.CLUE_SCROLL_REWARD_GROUP_ID): + // event type should be set via ChatMessage for clue scrolls. + // Clue Scrolls use same InventoryID as Barrows + container = client.getItemContainer(InventoryID.BARROWS_REWARD); + break; + default: + return; + } + + if (container == null) + { + return; + } + + // Convert container items to array of ItemStack + final ItemStack[] items = Arrays.stream(container.getItems()) + .map(item -> new ItemStack(item.getId(), item.getQuantity())) + .toArray(ItemStack[]::new); + + if (items.length > 0) + { + log.debug("Loot Received from Event: {}", eventType); + for (ItemStack item : items) + { + log.debug("Item Received: {}x {}", item.getQuantity(), item.getId()); + } + SwingUtilities.invokeLater(() -> panel.addLog(eventType, -1, items)); + } + else + { + log.debug("No items to find for Event: {} | Container: {}", eventType, container); + } + } + + @Subscribe + public void onChatMessage(ChatMessage event) + { + if (event.getType() != ChatMessageType.SERVER && event.getType() != ChatMessageType.FILTERED) + { + return; + } + + // Check if message is for a clue scroll reward + final Matcher m = CLUE_SCROLL_PATTERN.matcher(Text.removeTags(event.getMessage())); + if (m.find()) + { + final String type = m.group(1).toLowerCase(); + switch (type) + { + case "easy": + eventType = "Clue Scroll (Easy)"; + break; + case "medium": + eventType = "Clue Scroll (Medium)"; + break; + case "hard": + eventType = "Clue Scroll (Hard)"; + break; + case "elite": + eventType = "Clue Scroll (Elite)"; + break; + case "master": + eventType = "Clue Scroll (Master)"; + break; + } + } + } +} diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/panel_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/panel_icon.png new file mode 100644 index 0000000000..cc49bf27dc Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/loottracker/panel_icon.png differ