package KondoKit import KondoKit.Helpers.addMouseListenerToAll import KondoKit.Helpers.formatHtmlLabelText import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.XPTrackerView.wrappedWidget import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR import KondoKit.plugin.Companion.WIDGET_COLOR import KondoKit.plugin.Companion.primaryColor import KondoKit.plugin.Companion.secondaryColor import plugin.api.API import rt4.* import java.awt.* import java.awt.Font import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.awt.image.BufferedImage import java.io.BufferedReader import java.io.InputStreamReader import java.net.HttpURLConnection import java.net.URL import java.nio.charset.StandardCharsets import java.text.DecimalFormat import javax.swing.* import kotlin.math.ceil object LootTrackerView { private const val SNAPSHOT_LIFESPAN = 10 const val BAG_ICON = 900; val npcDeathSnapshots = mutableMapOf() var gePriceMap = loadGEPrices() private val lootItemPanels = mutableMapOf>() private val npcKillCounts = mutableMapOf() private var totalTrackerWidget: XPWidget? = null var lastConfirmedKillNpcId = -1 var customToolTipWindow: JWindow? = null var lootTrackerView: JPanel? = null fun loadGEPrices(): Map { return if (plugin.useLiveGEPrices) { try { println("LootTracker: Loading Remote GE Prices") val url = URL("https://cdn.2009scape.org/gedata/latest.json") val connection = url.openConnection() as HttpURLConnection connection.requestMethod = "GET" connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0") val responseCode = connection.responseCode if (responseCode == HttpURLConnection.HTTP_OK) { val inputStream = connection.inputStream val content = inputStream.bufferedReader().use(BufferedReader::readText) val items = content.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" } val gePrices = mutableMapOf() for (item in items) { val pairs = item.removeSurrounding("{", "}").split(",") val itemId = pairs.find { it.trim().startsWith("\"item_id\"") }?.split(":")?.get(1)?.trim()?.trim('\"') val value = pairs.find { it.trim().startsWith("\"value\"") }?.split(":")?.get(1)?.trim()?.trim('\"') if (itemId != null && value != null) { gePrices[itemId] = value } } gePrices } else { emptyMap() } } catch (e: Exception) { emptyMap() } } else { try { println("LootTracker: Loading Local GE Prices") BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/item_configs.json"), StandardCharsets.UTF_8)) .useLines { lines -> val json = lines.joinToString("\n") val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" } val gePrices = mutableMapOf() for (item in items) { val pairs = item.removeSurrounding("{", "}").split(",") val id = pairs.find { it.trim().startsWith("\"id\"") }?.split(":")?.get(1)?.trim()?.trim('\"') val grandExchangePrice = pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()?.trim('\"') if (id != null && grandExchangePrice != null) { gePrices[id] = grandExchangePrice } } gePrices } } catch (e: Exception) { emptyMap() } } } fun createLootTrackerView() { lootTrackerView = JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) // Use BoxLayout on Y axis to stack widgets vertically background = VIEW_BACKGROUND_COLOR add(Box.createVerticalStrut(5)) totalTrackerWidget = createTotalLootWidget() val wrapped = wrappedWidget(totalTrackerWidget!!.container) val popupMenu = resetLootTrackerMenu() // Create a custom MouseListener val rightClickListener = object : MouseAdapter() { override fun mousePressed(e: MouseEvent) { if (e.isPopupTrigger) { popupMenu.show(e.component, e.x, e.y) } } override fun mouseReleased(e: MouseEvent) { if (e.isPopupTrigger) { popupMenu.show(e.component, e.x, e.y) } } } addMouseListenerToAll(wrapped,rightClickListener) wrapped.addMouseListener(rightClickListener) add(wrapped) add(Box.createVerticalStrut(10)) revalidate() repaint() } } private fun updateTotalKills() { totalTrackerWidget?.let { it.totalXpGained += 1 it.xpGainedLabel.text = formatHtmlLabelText("Total Count: ", primaryColor, it.totalXpGained.toString(), secondaryColor) it.xpGainedLabel.repaint() } } private fun updateTotalValue(newVal: Int) { totalTrackerWidget?.let { it.previousXp += newVal it.xpPerHourLabel.text = formatHtmlLabelText("Total Value: ", primaryColor, formatValue(it.previousXp) + " gp", secondaryColor) it.container.repaint() } } private fun createTotalLootWidget(): XPWidget { val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(BAG_ICON)) val l1 = createLabel(formatHtmlLabelText("Total Value: ", primaryColor, "0" + " gp", secondaryColor)) val l2 = createLabel(formatHtmlLabelText("Total Count: ", primaryColor, "0", secondaryColor)) return XPWidget( skillId = -1, container = createWidgetPanel(bufferedImageSprite,l2,l1), xpGainedLabel = l2, xpLeftLabel = JLabel(), actionsRemainingLabel = JLabel(), xpPerHourLabel = l1, progressBar = ProgressBar(0.0, Color(150, 50, 50)), totalXpGained = 0, startTime = System.currentTimeMillis(), previousXp = 0 ) } private fun createWidgetPanel(bufferedImageSprite: BufferedImage, l1 : JLabel, l2 : JLabel): Panel { val imageCanvas = ImageCanvas(bufferedImageSprite).apply { size = Dimension(width, height) background = WIDGET_COLOR } val imageContainer = Panel(FlowLayout()).apply { background = WIDGET_COLOR add(imageCanvas) } return Panel(BorderLayout(5, 5)).apply { background = WIDGET_COLOR preferredSize = TOTAL_XP_WIDGET_SIZE add(imageContainer, BorderLayout.WEST) add(createTextPanel(l1,l2), BorderLayout.CENTER) } } private fun createTextPanel(l1 : JLabel, l2: JLabel): Panel { return Panel(GridLayout(2, 1, 5, 5)).apply { background = WIDGET_COLOR add(l1) add(l2) } } private fun createLabel(text: String): JLabel { return JLabel(text).apply { font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) horizontalAlignment = JLabel.LEFT } } private fun addItemToLootPanel(lootTrackerView: JPanel, drop: Item, npcName: String) { findLootItemsPanel(lootTrackerView, npcName)?.let { lootPanel -> val itemQuantities = lootItemPanels.getOrPut(npcName) { mutableMapOf() } val newQuantity = itemQuantities.merge(drop.id, drop.quantity, Int::plus) ?: drop.quantity lootPanel.components.filterIsInstance().find { it.getClientProperty("itemId") == drop.id } ?.let { updateItemPanelIcon(it, drop.id, newQuantity) } ?: addNewItemToPanel(lootPanel, drop, newQuantity) // Recalculate lootPanel size based on the number of unique items. val totalItems = lootItemPanels[npcName]?.size ?: 0 val rowsNeeded = ceil(totalItems / 6.0).toInt() val lootPanelHeight = rowsNeeded * (40) val size = Dimension(lootPanel.width,lootPanelHeight+32) lootPanel.parent.preferredSize = size lootPanel.parent.minimumSize = size lootPanel.parent.maximumSize = size lootPanel.parent.revalidate() lootPanel.parent.repaint() lootPanel.revalidate() lootPanel.repaint() } } private fun addNewItemToPanel(lootPanel: JPanel, drop: Item, newQuantity: Int) { val itemPanel = createItemPanel(drop.id, newQuantity) lootPanel.add(itemPanel) } private fun createItemPanel(itemId: Int, quantity: Int): JPanel { val bufferedImageSprite = getBufferedImageFromSprite(API.GetObjSprite(itemId, quantity, true, 0, 0)) // Create the panel for the item val itemPanel = FixedSizePanel(Dimension(36, 32)).apply { preferredSize = Dimension(36, 32) background = WIDGET_COLOR minimumSize = preferredSize maximumSize = preferredSize val imageCanvas = ImageCanvas(bufferedImageSprite).apply { preferredSize = Dimension(36, 32) background = WIDGET_COLOR minimumSize = preferredSize maximumSize = preferredSize } // Add the imageCanvas to the panel add(imageCanvas, BorderLayout.CENTER) // Put the itemId as a property for reference putClientProperty("itemId", itemId) // Add mouse listener for custom hover text imageCanvas.addMouseListener(object : MouseAdapter() { override fun mouseEntered(e: MouseEvent) { // Show custom tooltip when the mouse enters the component showCustomToolTip(e.point, itemId,quantity,imageCanvas) } override fun mouseExited(e: MouseEvent) { // Hide tooltip when mouse exits hideCustomToolTip() } }) } return itemPanel } // Function to show the custom tooltip fun showCustomToolTip(location: Point, itemId: Int, quantity: Int, parentComponent: ImageCanvas) { var itemDef = ObjTypeList.get(itemId) val gePricePerItem = gePriceMap[itemDef.id.toString()]?.toInt() ?: 0 val totalGePrice = gePricePerItem * quantity val totalHaPrice = itemDef.cost * quantity val geText = if (quantity > 1) " (${gePricePerItem} ea)" else "" val haText = if (quantity > 1) " (${itemDef.cost} ea)" else "" val text = "
" + "${itemDef.name} x $quantity
" + "GE: $totalGePrice ${geText}
" + "HA: $totalHaPrice ${haText}
" val _font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) val c = Color(50,50,50) if (customToolTipWindow == null) { customToolTipWindow = JWindow().apply { contentPane = JLabel(text).apply { border = BorderFactory.createLineBorder(Color.BLACK) isOpaque = true background = c foreground = Color.WHITE font = _font } pack() } } // Calculate the tooltip location relative to the parent component val screenLocation = parentComponent.locationOnScreen customToolTipWindow!!.setLocation(screenLocation.x + location.x, screenLocation.y + location.y + 20) customToolTipWindow!!.isVisible = true } // Function to hide the custom tooltip fun hideCustomToolTip() { customToolTipWindow?.isVisible = false customToolTipWindow = null // Nullify the global instance } private fun updateItemPanelIcon(panel: JPanel, itemId: Int, quantity: Int) { panel.removeAll() panel.add(createItemPanel(itemId, quantity).components[0], BorderLayout.CENTER) panel.revalidate() panel.repaint() } private fun updateKillCountLabel(lootTrackerPanel: JPanel, npcName: String) { lootTrackerPanel.components.filterIsInstance().forEach { childFramePanel -> (childFramePanel.components.firstOrNull { it is JPanel } as? JPanel)?.components ?.filterIsInstance()?.find { it.name == "killCountLabel_$npcName" } ?.apply { text = formatHtmlLabelText(npcName, secondaryColor, " x ${npcKillCounts[npcName]}", primaryColor) revalidate() repaint() } } } private fun updateValueLabel(lootTrackerPanel: JPanel, valueOfNewDrops: String, npcName: String) { lootTrackerPanel.components.filterIsInstance().forEach { childFramePanel -> (childFramePanel.components.firstOrNull { it is JPanel } as? JPanel)?.components ?.filterIsInstance()?.find { it.name == "valueLabel_$npcName" } ?.apply { val newValue = (getClientProperty("val") as? Int ?: 0) + valueOfNewDrops.toInt() text = "${formatValue(newValue)} gp" putClientProperty("val", newValue) revalidate() repaint() } } } private fun formatValue(value: Int): String { return when { value >= 1_000_000 -> "%.1fM".format(value / 1_000_000.0) value >= 10_000 -> "%.1fK".format(value / 1_000.0) else -> DecimalFormat("#,###").format(value) } } private fun findLootItemsPanel(container: Container, npcName: String): JPanel? { return container.components.filterIsInstance().firstOrNull { it.name == "LOOT_ITEMS_$npcName" } ?: container.components.filterIsInstance().mapNotNull { findLootItemsPanel(it, npcName) }.firstOrNull() } fun onPostClientTick(lootTrackerView: JPanel) { val toRemove = mutableListOf() npcDeathSnapshots.entries.forEach { (npcId, snapshot) -> val postDeathSnapshot = takeGroundSnapshot(Pair(snapshot.location.first, snapshot.location.second)) val newDrops = postDeathSnapshot.subtract(snapshot.items) if (newDrops.isNotEmpty()) { val npcName = NpcTypeList.get(npcId).name lastConfirmedKillNpcId = npcId; handleNewDrops(npcName.toString(), newDrops, lootTrackerView) toRemove.add(npcId) } else if (snapshot.age >= SNAPSHOT_LIFESPAN) { toRemove.add(npcId) } else { snapshot.age++ } } toRemove.forEach { npcDeathSnapshots.remove(it) } } fun takeGroundSnapshot(location: Pair): Set { return getGroundItemsAt(location.first, location.second).toSet() } private fun getGroundItemsAt(x: Int, z: Int): List { val objstacknodeLL = SceneGraph.objStacks[Player.plane][x][z] ?: return emptyList() val items = mutableListOf() var itemNode = objstacknodeLL.head() as ObjStackNode? while (itemNode != null) { items.add(Item(itemNode.value.type, itemNode.value.amount)) itemNode = objstacknodeLL.next() as ObjStackNode? } return items } private fun handleNewDrops(npcName: String, newDrops: Set, lootTrackerView: JPanel) { findLootItemsPanel(lootTrackerView, npcName)?.let { } ?: run { lootTrackerView.add(createLootFrame(npcName)) lootTrackerView.add(Box.createVerticalStrut(10)) lootTrackerView.revalidate() lootTrackerView.repaint() } npcKillCounts[npcName] = npcKillCounts.getOrDefault(npcName, 0) + 1 updateKillCountLabel(lootTrackerView, npcName) updateTotalKills() newDrops.forEach { drop -> val geValue = (gePriceMap[drop.id.toString()]?.toInt() ?: 0) * drop.quantity updateValueLabel(lootTrackerView, geValue.toString(), npcName) addItemToLootPanel(lootTrackerView, drop, npcName) updateTotalValue(geValue) } } private fun createLootFrame(npcName: String): JPanel { val childFramePanel = JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) background = WIDGET_COLOR minimumSize = Dimension(230, 0) maximumSize = Dimension(230, 700) name = "HELLO_WORLD" } val labelPanel = JPanel(BorderLayout()).apply { background = Color(21, 21, 21) border = BorderFactory.createEmptyBorder(5, 5, 5, 5) maximumSize = Dimension(230, 24) minimumSize = maximumSize preferredSize = maximumSize } val killCount = npcKillCounts.getOrPut(npcName) { 0 } val countLabel = JLabel(formatHtmlLabelText(npcName, secondaryColor, " x $killCount", primaryColor)).apply { foreground = Color(200, 200, 200) font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) horizontalAlignment = JLabel.LEFT name = "killCountLabel_$npcName" } val valueLabel = JLabel("0 gp").apply { foreground = Color(200, 200, 200) font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) horizontalAlignment = JLabel.RIGHT name = "valueLabel_$npcName" } labelPanel.add(countLabel, BorderLayout.WEST) labelPanel.add(valueLabel, BorderLayout.EAST) // Panel to hold loot items, using GridLayout to manage rows and columns. val lootPanel = JPanel().apply { background = WIDGET_COLOR border = BorderFactory.createLineBorder(WIDGET_COLOR, 5) name = "LOOT_ITEMS_$npcName" layout = GridLayout(0, 6, 1, 1) // 6 columns, rows will be added dynamically } lootItemPanels[npcName] = mutableMapOf() childFramePanel.add(labelPanel) childFramePanel.add(lootPanel) val popupMenu = removeLootFrameMenu(childFramePanel, npcName) // Create a custom MouseListener val rightClickListener = object : MouseAdapter() { override fun mousePressed(e: MouseEvent) { if (e.isPopupTrigger) { popupMenu.show(e.component, e.x, e.y) } } override fun mouseReleased(e: MouseEvent) { if (e.isPopupTrigger) { popupMenu.show(e.component, e.x, e.y) } } } labelPanel.addMouseListener(rightClickListener) return childFramePanel } fun removeLootFrameMenu(toRemove: JPanel, npcName: String): JPopupMenu { // Create a popup menu val popupMenu = JPopupMenu() val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) popupMenu.background = Color(45, 45, 45) // Create menu items with custom font and colors val menuItem1 = JMenuItem("Remove").apply { font = rFont // Set custom font background = Color(45, 45, 45) // Dark background for item foreground = Color(220, 220, 220) // Light text color for item } popupMenu.add(menuItem1) menuItem1.addActionListener { lootItemPanels[npcName]?.clear() npcKillCounts[npcName] = 0 lootTrackerView?.let { parent -> val components = parent.components val toRemoveIndex = components.indexOf(toRemove) if (toRemoveIndex >= 0 && toRemoveIndex < components.size - 1) { val nextComponent = components[toRemoveIndex + 1] if (nextComponent is Box.Filler) { // Nasty way to remove the Box.createVerticalStrut(10) after // the lootpanel. parent.remove(nextComponent) } } parent.remove(toRemove) parent.revalidate() parent.repaint() } } return popupMenu } fun resetLootTrackerMenu(): JPopupMenu { // Create a popup menu val popupMenu = JPopupMenu() val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) popupMenu.background = Color(45, 45, 45) // Create menu items with custom font and colors val menuItem1 = JMenuItem("Reset Loot Tracker").apply { font = rFont // Set custom font background = Color(45, 45, 45) // Dark background for item foreground = Color(220, 220, 220) // Light text color for item } popupMenu.add(menuItem1) menuItem1.addActionListener { plugin.registerDrawAction { resetLootTracker() } } return popupMenu } private fun resetLootTracker(){ lootTrackerView?.removeAll() npcKillCounts.clear() lootItemPanels.clear() totalTrackerWidget = createTotalLootWidget() val wrapped = wrappedWidget(totalTrackerWidget!!.container) val _popupMenu = resetLootTrackerMenu() // Create a custom MouseListener val rightClickListener = object : MouseAdapter() { override fun mousePressed(e: MouseEvent) { if (e.isPopupTrigger) { _popupMenu.show(e.component, e.x, e.y) } } override fun mouseReleased(e: MouseEvent) { if (e.isPopupTrigger) { _popupMenu.show(e.component, e.x, e.y) } } } addMouseListenerToAll(wrapped,rightClickListener) wrapped.addMouseListener(rightClickListener) lootTrackerView?.add(Box.createVerticalStrut(5)) lootTrackerView?.add(wrapped) lootTrackerView?.add(Box.createVerticalStrut(10)) lootTrackerView?.revalidate() lootTrackerView?.repaint() } class FixedSizePanel(private val fixedSize: Dimension) : JPanel() { override fun getPreferredSize(): Dimension { return fixedSize } override fun getMinimumSize(): Dimension { return fixedSize } override fun getMaximumSize(): Dimension { return fixedSize } } data class GroundSnapshot(val items: Set, val location: Pair, var age: Int) data class Item(val id: Int, val quantity: Int) }