From b8c7a1150fc3b4754fdd873fc4a94ea6cee23697 Mon Sep 17 00:00:00 2001 From: downthecrop Date: Tue, 8 Oct 2024 01:20:42 -0700 Subject: [PATCH] Kondo 2.0 --- .../src/main/kotlin/KondoKit/Helpers.kt | 170 +++++++- .../src/main/kotlin/KondoKit/HiscoresView.kt | 110 ++++-- .../src/main/kotlin/KondoKit/ImageCanvas.kt | 13 +- .../src/main/kotlin/KondoKit/KondoKitUtils.kt | 100 ----- .../main/kotlin/KondoKit/LootTrackerView.kt | 264 +++++++++++-- .../src/main/kotlin/KondoKit/ProgressBar.kt | 42 +- .../kotlin/KondoKit/ReflectiveEditorView.kt | 315 ++++++++++----- .../main/kotlin/KondoKit/ScrollablePanel.kt | 154 ++++++++ .../src/main/kotlin/KondoKit/XPTrackerView.kt | 207 +++++++--- .../src/main/kotlin/KondoKit/plugin.kt | 362 ++++++++++++------ .../main/kotlin/KondoKit/plugin.properties | 2 +- .../KondoKit/{ => res}/item_configs.json | 0 .../KondoKit/{ => res}/npc_hitpoints_map.json | 0 .../kotlin/KondoKit/res/runescape_small.ttf | Bin 0 -> 33260 bytes 14 files changed, 1265 insertions(+), 474 deletions(-) delete mode 100644 plugin-playground/src/main/kotlin/KondoKit/KondoKitUtils.kt create mode 100644 plugin-playground/src/main/kotlin/KondoKit/ScrollablePanel.kt rename plugin-playground/src/main/kotlin/KondoKit/{ => res}/item_configs.json (100%) rename plugin-playground/src/main/kotlin/KondoKit/{ => res}/npc_hitpoints_map.json (100%) create mode 100644 plugin-playground/src/main/kotlin/KondoKit/res/runescape_small.ttf diff --git a/plugin-playground/src/main/kotlin/KondoKit/Helpers.kt b/plugin-playground/src/main/kotlin/KondoKit/Helpers.kt index a606805..066edd1 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/Helpers.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/Helpers.kt @@ -1,10 +1,165 @@ package KondoKit -import java.awt.Color -import java.awt.Dimension -import javax.swing.JPanel +import rt4.GameShell +import java.awt.* +import java.awt.event.MouseListener +import java.lang.reflect.Field +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.util.* +import java.util.Timer +import javax.swing.* object Helpers { + + fun convertValue(type: Class<*>, genericType: Type?, value: String): Any { + return when { + type == Int::class.java -> value.toInt() + type == Double::class.java -> value.toDouble() + type == Boolean::class.java -> value.toBoolean() + type == Color::class.java -> convertToColor(value) + type == List::class.java && genericType is ParameterizedType -> { + val actualTypeArgument = genericType.actualTypeArguments.firstOrNull() + when { + value.isBlank() -> emptyList() // Handle empty string by returning an empty list + actualTypeArgument == Int::class.javaObjectType -> value.trim('[', ']').split(",").filter { it.isNotBlank() }.map { it.trim().toInt() } + actualTypeArgument == String::class.java -> value.trim('[', ']').split(",").filter { it.isNotBlank() }.map { it.trim() } + else -> throw IllegalArgumentException("Unsupported List type: $actualTypeArgument") + } + } + else -> value // Default to String + } + } + + fun showToast( + parentComponent: Component?, + message: String, + messageType: Int = JOptionPane.INFORMATION_MESSAGE + ) { + SwingUtilities.invokeLater { + val toast = JWindow() + toast.type = Window.Type.POPUP + toast.background = Color(0, 0, 0, 0) + + val panel = JPanel() + panel.isOpaque = false + panel.layout = BoxLayout(panel, BoxLayout.Y_AXIS) + + val label = JLabel(message) + label.foreground = Color.WHITE + + label.background = when (messageType) { + JOptionPane.ERROR_MESSAGE -> Color(220, 20, 60, 230) // Crimson for errors + JOptionPane.INFORMATION_MESSAGE -> Color(0, 128, 0, 230) // Green for success + JOptionPane.WARNING_MESSAGE -> Color(255, 165, 0, 230) // Orange for warnings + else -> Color(0, 0, 0, 170) // Default semi-transparent black + } + + label.isOpaque = true + label.border = BorderFactory.createEmptyBorder(10, 20, 10, 20) + label.maximumSize = Dimension(242, 50) + label.preferredSize = Dimension(242, 50) + panel.add(label) + + + + toast.contentPane.add(panel) + toast.pack() + + + // Adjust for parent component location if it exists + if (parentComponent != null) { + val parentLocation = parentComponent.locationOnScreen + val x = parentLocation.x + val y = GameShell.canvas.locationOnScreen.y + toast.setLocation(x, y) + } else { + // Fallback to screen center if no parent is provided + val screenSize = Toolkit.getDefaultToolkit().screenSize + val x = (screenSize.width - toast.width) / 2 + val y = screenSize.height - toast.height - 50 + toast.setLocation(x, y) + } + + toast.isVisible = true + + Timer().schedule(object : TimerTask() { + override fun run() { + SwingUtilities.invokeLater { + toast.isVisible = false + toast.dispose() + } + } + }, 2000) + } + } + + + + fun convertToColor(value: String): Color { + val color = Color.decode(value) // Assumes value is in format "#RRGGBB" or "0xRRGGBB" + return color + } + + fun colorToHex(color: Color): String { + return "#%02x%02x%02x".format(color.red, color.green, color.blue) + } + + fun colorToIntArray(color: Color): IntArray { + return intArrayOf(color.red, color.green, color.blue) + } + + interface FieldObserver { + fun onFieldChange(field: Field, newValue: Any?) + } + + fun addMouseListenerToAll(container: Container, listener: MouseListener) { + // Recursively go through all components within the container + for (component in container.components) { + // Add the passed MouseListener to the component + if (component is JComponent || component is Canvas) { + component.addMouseListener(listener) + } + + // If the component is a container, recursively call this function + if (component is Container) { + addMouseListenerToAll(component, listener) + } + } + } + + + + + class FieldNotifier(private val plugin: Any) { + private val observers = mutableListOf() + + fun addObserver(observer: FieldObserver) { + observers.add(observer) + } + + fun notifyFieldChange(field: Field, newValue: Any?) { + for (observer in observers) { + observer.onFieldChange(field, newValue) + } + } + + fun setFieldValue(field: Field, value: Any?) { + field.isAccessible = true + field.set(plugin, value) + notifyFieldChange(field, value) + + try { + val onUpdateMethod = plugin::class.java.getMethod("OnKondoValueUpdated") + onUpdateMethod.invoke(plugin) + } catch (e: NoSuchMethodException) { + // The method doesn't exist + } catch (e: Exception) { + e.printStackTrace() + } + } + } + fun getSpriteId(skillId: Int) : Int { return when (skillId) { 0 -> 197 @@ -78,13 +233,4 @@ object Helpers { else -> Color(128, 128, 128) // Default grey for unhandled skill IDs } } - - class Spacer(width: Int = 0, height: Int = 0) : JPanel() { - init { - preferredSize = Dimension(width, height) - maximumSize = preferredSize - minimumSize = preferredSize - isOpaque = false - } - } } \ No newline at end of file diff --git a/plugin-playground/src/main/kotlin/KondoKit/HiscoresView.kt b/plugin-playground/src/main/kotlin/KondoKit/HiscoresView.kt index e1e0f77..3ceb86d 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/HiscoresView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/HiscoresView.kt @@ -1,9 +1,16 @@ package KondoKit import KondoKit.Constants.COLOR_BACKGROUND_DARK +import KondoKit.Constants.SKILL_DISPLAY_ORDER +import KondoKit.Constants.SKILL_SPRITE_DIMENSION +import KondoKit.Helpers.formatHtmlLabelText import KondoKit.Helpers.getSpriteId +import KondoKit.Helpers.showToast import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR +import KondoKit.plugin.Companion.WIDGET_COLOR +import KondoKit.plugin.Companion.primaryColor +import KondoKit.plugin.Companion.secondaryColor import com.google.gson.Gson import plugin.api.API import rt4.Sprites @@ -16,6 +23,7 @@ import java.awt.event.MouseEvent import java.io.BufferedReader import java.io.InputStreamReader import java.net.HttpURLConnection +import java.net.SocketTimeoutException import java.net.URL import javax.swing.* import javax.swing.border.MatteBorder @@ -28,35 +36,38 @@ object Constants { const val LVL_BAR_SPRITE = 898 // Dimensions - val SEARCH_FIELD_DIMENSION = Dimension(270, 30) + val SEARCH_FIELD_DIMENSION = Dimension(230, 30) val ICON_DIMENSION_SMALL = Dimension(12, 12) val ICON_DIMENSION_MEDIUM = Dimension(18, 20) val ICON_DIMENSION_LARGE = Dimension(30, 30) - val HISCORE_PANEL_DIMENSION = Dimension(270, 400) - val FILTER_PANEL_DIMENSION = Dimension(270, 30) - val SKILLS_PANEL_DIMENSION = Dimension(300, 300) - val TOTAL_COMBAT_PANEL_DIMENSION = Dimension(270, 30) - val SKILL_PANEL_DIMENSION = Dimension(90, 35) + val HISCORE_PANEL_DIMENSION = Dimension(230, 500) + val FILTER_PANEL_DIMENSION = Dimension(230, 30) + val SKILLS_PANEL_DIMENSION = Dimension(230, 290) + val TOTAL_COMBAT_PANEL_DIMENSION = Dimension(230, 30) + val SKILL_PANEL_DIMENSION = Dimension(76, 35) val IMAGE_CANVAS_DIMENSION = Dimension(20, 20) - val NUMBER_LABEL_DIMENSION = Dimension(30, 20) + val SKILL_SPRITE_DIMENSION = Dimension(14, 14) + val NUMBER_LABEL_DIMENSION = Dimension(20, 20) // Colors val COLOR_BACKGROUND_DARK = Color(27, 27, 27) - val COLOR_BACKGROUND_MEDIUM = Color(37, 37, 37) - val COLOR_BACKGROUND_LIGHT = Color(43, 43, 43) + val COLOR_BACKGROUND_MEDIUM = VIEW_BACKGROUND_COLOR val COLOR_FOREGROUND_LIGHT = Color(200, 200, 200) val COLOR_RED = Color.RED val COLOR_SKILL_PANEL = Color(60, 60, 60) // Fonts val FONT_ARIAL_PLAIN_14 = Font("Arial", Font.PLAIN, 14) - val FONT_ARIAL_PLAIN_12 = Font("Arial", Font.PLAIN, 12) + val FONT_ARIAL_PLAIN_12 = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) val FONT_ARIAL_BOLD_12 = Font("Arial", Font.BOLD, 12) + val SKILL_DISPLAY_ORDER = arrayOf(0,3,14,2,16,13,1,15,10,4,17,7,5,12,11,6,9,8,20,18,19,22,21,23) } var text: String = "" object HiscoresView { + + var hiScoreView: JPanel? = null class CustomSearchField(private val hiscoresPanel: JPanel) : Canvas() { private var cursorVisible: Boolean = true @@ -69,6 +80,7 @@ object HiscoresView { size = preferredSize minimumSize = preferredSize maximumSize = preferredSize + fillColor = COLOR_BACKGROUND_DARK } } @@ -125,7 +137,7 @@ object HiscoresView { } }) - Timer(500) { + Timer(1000) { cursorVisible = !cursorVisible if(plugin.StateManager.focusedView == "HISCORE_SEARCH_VIEW") repaint() @@ -140,7 +152,7 @@ object HiscoresView { val fm = g.fontMetrics val cursorX = fm.stringWidth(text) + 30 - imageCanvas?.let { canvas -> + imageCanvas.let { canvas -> val imgG = g.create(5, 5, canvas.width, canvas.height) canvas.paint(imgG) imgG.dispose() @@ -159,8 +171,10 @@ object HiscoresView { } fun searchPlayer(username: String) { - text = username - val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${username.toLowerCase()}" + text = username.replace(" ", "_") + val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${text.toLowerCase()}" + + updateHiscoresView(null, "Searching...") Thread { try { @@ -168,6 +182,10 @@ object HiscoresView { val connection = url.openConnection() as HttpURLConnection connection.requestMethod = "GET" + // If a request take longer than 5 seconds timeout. + connection.connectTimeout = 5000 + connection.readTimeout = 5000 + val responseCode = connection.responseCode if (responseCode == HttpURLConnection.HTTP_OK) { val reader = BufferedReader(InputStreamReader(connection.inputStream)) @@ -179,27 +197,45 @@ object HiscoresView { } } else { SwingUtilities.invokeLater { - showError("Player not found!") + showToast(hiscoresPanel, "Player not found!", JOptionPane.ERROR_MESSAGE) } } - } catch (e: Exception) { + } catch (e: SocketTimeoutException) { SwingUtilities.invokeLater { - showError("Error fetching data!") + showToast(hiscoresPanel, "Request timed out", JOptionPane.ERROR_MESSAGE) + } + } catch (e: Exception) { + // Handle other errors + SwingUtilities.invokeLater { + showToast(hiscoresPanel, "Error fetching data!", JOptionPane.ERROR_MESSAGE) } } }.start() } + private fun updatePlayerData(jsonResponse: String, username: String) { val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java) updateHiscoresView(hiscoresResponse, username) } - private fun updateHiscoresView(data: HiscoresResponse, username: String) { + private fun updateHiscoresView(data: HiscoresResponse?, username: String) { val playerNameLabel = findComponentByName(hiscoresPanel, "playerNameLabel") as? JPanel - val ironMode = data.info.iron_mode - playerNameLabel?.removeAll() // Clear previous components + var nameLabel = JLabel(formatHtmlLabelText(username, secondaryColor, "", primaryColor), JLabel.CENTER).apply { + font = Constants.FONT_ARIAL_BOLD_12 + foreground = Constants.COLOR_FOREGROUND_LIGHT + border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding + } + playerNameLabel?.add(nameLabel) + playerNameLabel?.revalidate() + playerNameLabel?.repaint() + + if(data == null) return; + + playerNameLabel?.removeAll() + + val ironMode = data.info.iron_mode if (ironMode != "0") { val ironmanBufferedImage = getBufferedImageFromSprite(Sprites.nameIcons[Constants.IRONMAN_SPRITE + ironMode.toInt() - 1]) @@ -213,11 +249,14 @@ object HiscoresView { playerNameLabel?.add(imageCanvas) } - val nameLabel = JLabel(username, JLabel.CENTER).apply { + val exp_multiplier = data.info.exp_multiplier + nameLabel = JLabel(formatHtmlLabelText(username, secondaryColor, " (${exp_multiplier}x)", primaryColor), JLabel.CENTER).apply { font = Constants.FONT_ARIAL_BOLD_12 foreground = Constants.COLOR_FOREGROUND_LIGHT + border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding } + playerNameLabel?.add(nameLabel) playerNameLabel?.revalidate() @@ -296,7 +335,7 @@ object HiscoresView { } } - fun createHiscoreSearchView(): JPanel { + fun createHiscoreSearchView() { val hiscorePanel = JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) name = "HISCORE_SEARCH_VIEW" @@ -324,14 +363,13 @@ object HiscoresView { add(searchFieldWrapper) } - hiscorePanel.add(Helpers.Spacer(height = 10)) + hiscorePanel.add(Box.createVerticalStrut(10)) hiscorePanel.add(searchPanel) - hiscorePanel.add(Helpers.Spacer(height = 10)) + hiscorePanel.add(Box.createVerticalStrut(10)) - // Adding the player name panel in place of the filterPanel val playerNamePanel = JPanel().apply { - layout = FlowLayout(FlowLayout.CENTER) - background = VIEW_BACKGROUND_COLOR + layout = GridBagLayout() // This will center the JLabel both vertically and horizontally + background = WIDGET_COLOR preferredSize = Constants.FILTER_PANEL_DIMENSION maximumSize = preferredSize minimumSize = preferredSize @@ -339,7 +377,7 @@ object HiscoresView { } hiscorePanel.add(playerNamePanel) - hiscorePanel.add(Helpers.Spacer(height = 10)) + hiscorePanel.add(Box.createVerticalStrut(10)) val skillsPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply { background = Constants.COLOR_BACKGROUND_MEDIUM @@ -348,10 +386,10 @@ object HiscoresView { minimumSize = preferredSize } - for (i in 0 until 24) { + for (i in SKILL_DISPLAY_ORDER) { val skillPanel = JPanel().apply { layout = BorderLayout() - background = Constants.COLOR_SKILL_PANEL + background = COLOR_BACKGROUND_DARK preferredSize = Constants.SKILL_PANEL_DIMENSION maximumSize = preferredSize minimumSize = preferredSize @@ -362,8 +400,9 @@ object HiscoresView { val imageCanvas = bufferedImageSprite.let { ImageCanvas(it).apply { - preferredSize = Constants.IMAGE_CANVAS_DIMENSION - size = Constants.IMAGE_CANVAS_DIMENSION + preferredSize = SKILL_SPRITE_DIMENSION + size = SKILL_SPRITE_DIMENSION + fillColor = COLOR_BACKGROUND_DARK } } @@ -376,7 +415,7 @@ object HiscoresView { } val imageContainer = JPanel(FlowLayout(FlowLayout.CENTER, 5, 0)).apply { - background = Constants.COLOR_BACKGROUND_DARK + background = COLOR_BACKGROUND_DARK add(imageCanvas) add(numberLabel) } @@ -431,7 +470,7 @@ object HiscoresView { } val combatLevelPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply { - background = Constants.COLOR_BACKGROUND_DARK + background = COLOR_BACKGROUND_DARK add(combatLevelIcon) add(combatLevelLabel) } @@ -439,8 +478,9 @@ object HiscoresView { totalCombatPanel.add(totalLevelPanel) totalCombatPanel.add(combatLevelPanel) hiscorePanel.add(totalCombatPanel) + hiscorePanel.add(Box.createVerticalStrut(10)) - return hiscorePanel + hiScoreView = hiscorePanel; } data class HiscoresResponse( diff --git a/plugin-playground/src/main/kotlin/KondoKit/ImageCanvas.kt b/plugin-playground/src/main/kotlin/KondoKit/ImageCanvas.kt index 8923f08..3a9a9f3 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/ImageCanvas.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/ImageCanvas.kt @@ -1,5 +1,6 @@ package KondoKit +import KondoKit.plugin.Companion.WIDGET_COLOR import java.awt.Canvas import java.awt.Color import java.awt.Dimension @@ -8,31 +9,25 @@ import java.awt.image.BufferedImage class ImageCanvas(private val image: BufferedImage) : Canvas() { + var fillColor: Color = WIDGET_COLOR + init { - // Manually set the alpha value to 255 (fully opaque) only for pixels that are not fully transparent val width = image.width val height = image.height for (y in 0 until height) { for (x in 0 until width) { - // Retrieve the current pixel color val color = image.getRGB(x, y) - - // Check if the pixel is not fully transparent (i.e., color is not 0) if (color != 0) { - // Ensure the alpha is set to 255 (fully opaque) val newColor = (color and 0x00FFFFFF) or (0xFF shl 24) - - // Set the pixel with the updated color image.setRGB(x, y, newColor) } } } } - override fun paint(g: Graphics) { super.paint(g) - g.color = Color(27, 27, 27) + g.color = fillColor g.fillRect(0, 0, width, height) g.drawImage(image, 0, 0, width, height, this) } diff --git a/plugin-playground/src/main/kotlin/KondoKit/KondoKitUtils.kt b/plugin-playground/src/main/kotlin/KondoKit/KondoKitUtils.kt deleted file mode 100644 index 3ef3c39..0000000 --- a/plugin-playground/src/main/kotlin/KondoKit/KondoKitUtils.kt +++ /dev/null @@ -1,100 +0,0 @@ -package KondoKit - -import java.awt.Color -import java.lang.reflect.Field -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type - - -/* - This is used for the runtime editing of plugin variables. - To expose fields name them starting with `kondoExposed_` - When they are applied this will trigger an invoke of OnKondoValueUpdated() - if it is implemented. Check GroundItems plugin for an example. - */ - -object KondoKitUtils { - const val KONDO_PREFIX = "kondoExposed_" - - fun isKondoExposed(field: Field): Boolean { - return field.name.startsWith(KONDO_PREFIX) - } - - fun getKondoExposedFields(instance: Any): List { - val exposedFields: MutableList = ArrayList() - for (field in instance.javaClass.declaredFields) { - if (isKondoExposed(field)) { - exposedFields.add(field) - } - } - return exposedFields - } - - fun convertValue(type: Class<*>, genericType: Type?, value: String): Any { - return when { - type == Int::class.java -> value.toInt() - type == Double::class.java -> value.toDouble() - type == Boolean::class.java -> value.toBoolean() - type == Color::class.java -> convertToColor(value) - type == List::class.java && genericType is ParameterizedType -> { - val actualTypeArgument = genericType.actualTypeArguments.firstOrNull() - when { - value.isBlank() -> emptyList() // Handle empty string by returning an empty list - actualTypeArgument == Int::class.javaObjectType -> value.trim('[', ']').split(",").filter { it.isNotBlank() }.map { it.trim().toInt() } - actualTypeArgument == String::class.java -> value.trim('[', ']').split(",").filter { it.isNotBlank() }.map { it.trim() } - else -> throw IllegalArgumentException("Unsupported List type: $actualTypeArgument") - } - } - else -> value // Default to String - } - } - - - - fun convertToColor(value: String): Color { - val color = Color.decode(value) // Assumes value is in format "#RRGGBB" or "0xRRGGBB" - return color - } - - fun colorToHex(color: Color): String { - return "#%02x%02x%02x".format(color.red, color.green, color.blue) - } - - fun colorToIntArray(color: Color): IntArray { - return intArrayOf(color.red, color.green, color.blue) - } - - interface FieldObserver { - fun onFieldChange(field: Field, newValue: Any?) - } - - class FieldNotifier(private val plugin: Any) { - private val observers = mutableListOf() - - fun addObserver(observer: FieldObserver) { - observers.add(observer) - } - - fun notifyFieldChange(field: Field, newValue: Any?) { - for (observer in observers) { - observer.onFieldChange(field, newValue) - } - } - - fun setFieldValue(field: Field, value: Any?) { - field.isAccessible = true - field.set(plugin, value) - notifyFieldChange(field, value) - - try { - val onUpdateMethod = plugin::class.java.getMethod("OnKondoValueUpdated") - onUpdateMethod.invoke(plugin) - } catch (e: Exception) { - e.printStackTrace() - } - } - - - } - -} diff --git a/plugin-playground/src/main/kotlin/KondoKit/LootTrackerView.kt b/plugin-playground/src/main/kotlin/KondoKit/LootTrackerView.kt index 9da6c6b..459b84f 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/LootTrackerView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/LootTrackerView.kt @@ -1,5 +1,6 @@ package KondoKit +import KondoKit.Helpers.addMouseListenerToAll import KondoKit.Helpers.formatHtmlLabelText import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.XPTrackerView.wrappedWidget @@ -9,11 +10,11 @@ import KondoKit.plugin.Companion.WIDGET_COLOR import KondoKit.plugin.Companion.primaryColor import KondoKit.plugin.Companion.secondaryColor import plugin.api.API -import rt4.NpcTypeList -import rt4.ObjStackNode -import rt4.Player -import rt4.SceneGraph +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 @@ -22,6 +23,7 @@ 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 @@ -31,10 +33,12 @@ object LootTrackerView { private val lootItemPanels = mutableMapOf>() private val npcKillCounts = mutableMapOf() private var totalTrackerWidget: XPWidget? = null - var lastConfirmedKillNpcId = -1; + var lastConfirmedKillNpcId = -1 + var customToolTipWindow: JWindow? = null + var lootTrackerView: JPanel? = null fun loadGEPrices(): Map { - return if (plugin.kondoExposed_useLiveGEPrices) { + return if (plugin.useLiveGEPrices) { try { println("LootTracker: Loading Remote GE Prices") val url = URL("https://cdn.2009scape.org/gedata/latest.json") @@ -69,7 +73,7 @@ object LootTrackerView { } else { try { println("LootTracker: Loading Local GE Prices") - BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("item_configs.json"), StandardCharsets.UTF_8)) + 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() + "}" } @@ -94,17 +98,34 @@ object LootTrackerView { - fun createLootTrackerView(): JPanel { - return JPanel().apply { - layout = FlowLayout(FlowLayout.CENTER, 0, 5) + fun createLootTrackerView() { + lootTrackerView = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) // Use BoxLayout on Y axis to stack widgets vertically background = VIEW_BACKGROUND_COLOR - preferredSize = Dimension(270, 700) - maximumSize = Dimension(270, 700) - minimumSize = Dimension(270, 700) add(Box.createVerticalStrut(5)) totalTrackerWidget = createTotalLootWidget() - add(wrappedWidget(totalTrackerWidget!!.panel)) - add(Helpers.Spacer(height = 15)) + + 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() } @@ -122,7 +143,7 @@ object LootTrackerView { totalTrackerWidget?.let { it.previousXp += newVal it.xpPerHourLabel.text = formatHtmlLabelText("Total Value: ", primaryColor, formatValue(it.previousXp) + " gp", secondaryColor) - it.panel.repaint() + it.container.repaint() } } @@ -132,7 +153,7 @@ object LootTrackerView { val l2 = createLabel(formatHtmlLabelText("Total Count: ", primaryColor, "0", secondaryColor)) return XPWidget( skillId = -1, - panel = createWidgetPanel(bufferedImageSprite,l2,l1), + container = createWidgetPanel(bufferedImageSprite,l2,l1), xpGainedLabel = l2, xpLeftLabel = JLabel(), actionsRemainingLabel = JLabel(), @@ -173,7 +194,7 @@ object LootTrackerView { private fun createLabel(text: String): JLabel { return JLabel(text).apply { - font = Font("Arial", Font.PLAIN, 11) + font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) horizontalAlignment = JLabel.LEFT } } @@ -189,9 +210,15 @@ object LootTrackerView { // Recalculate lootPanel size based on the number of unique items. val totalItems = lootItemPanels[npcName]?.size ?: 0 - val rowsNeeded = Math.ceil(totalItems / 6.0).toInt() - val lootPanelHeight = rowsNeeded * 36 + (rowsNeeded - 1) - lootPanel.preferredSize = Dimension(270, lootPanelHeight+10) + 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() @@ -205,21 +232,86 @@ object LootTrackerView { private fun createItemPanel(itemId: Int, quantity: Int): JPanel { val bufferedImageSprite = getBufferedImageFromSprite(API.GetObjSprite(itemId, quantity, true, 0, 0)) - return FixedSizePanel(Dimension(36, 32)).apply { + + // Create the panel for the item + val itemPanel = FixedSizePanel(Dimension(36, 32)).apply { preferredSize = Dimension(36, 32) background = WIDGET_COLOR minimumSize = preferredSize maximumSize = preferredSize - add(ImageCanvas(bufferedImageSprite).apply { + + val imageCanvas = ImageCanvas(bufferedImageSprite).apply { preferredSize = Dimension(36, 32) background = WIDGET_COLOR minimumSize = preferredSize maximumSize = preferredSize - }, BorderLayout.CENTER) + } + + // 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) @@ -308,9 +400,8 @@ object LootTrackerView { private fun handleNewDrops(npcName: String, newDrops: Set, lootTrackerView: JPanel) { findLootItemsPanel(lootTrackerView, npcName)?.let { } ?: run { - // Panel doesn't exist, so create and add it lootTrackerView.add(createLootFrame(npcName)) - lootTrackerView.add(Helpers.Spacer(height = 15)) + lootTrackerView.add(Box.createVerticalStrut(10)) lootTrackerView.revalidate() lootTrackerView.repaint() } @@ -330,14 +421,15 @@ object LootTrackerView { val childFramePanel = JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) background = WIDGET_COLOR - minimumSize = Dimension(270, 0) - maximumSize = Dimension(270, 700) + 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(270, 24) + maximumSize = Dimension(230, 24) minimumSize = maximumSize preferredSize = maximumSize } @@ -345,14 +437,14 @@ object LootTrackerView { val killCount = npcKillCounts.getOrPut(npcName) { 0 } val countLabel = JLabel(formatHtmlLabelText(npcName, secondaryColor, " x $killCount", primaryColor)).apply { foreground = Color(200, 200, 200) - font = Font("Arial", Font.PLAIN, 12) + 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("Arial", Font.PLAIN, 12) + font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) horizontalAlignment = JLabel.RIGHT name = "valueLabel_$npcName" } @@ -370,19 +462,115 @@ object LootTrackerView { lootItemPanels[npcName] = mutableMapOf() - // Determine number of items and adjust size of lootPanel - val totalItems = lootItemPanels[npcName]?.size ?: 0 - val rowsNeeded = Math.ceil(totalItems / 6.0).toInt() - val lootPanelHeight = rowsNeeded * 36 + (rowsNeeded - 1) // Height per row = 36 + spacing - lootPanel.preferredSize = Dimension(270, lootPanelHeight+10) - childFramePanel.add(labelPanel) childFramePanel.add(lootPanel) - 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 { + 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() + } + return popupMenu + } + class FixedSizePanel(private val fixedSize: Dimension) : JPanel() { override fun getPreferredSize(): Dimension { diff --git a/plugin-playground/src/main/kotlin/KondoKit/ProgressBar.kt b/plugin-playground/src/main/kotlin/KondoKit/ProgressBar.kt index 0554641..f5df980 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/ProgressBar.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/ProgressBar.kt @@ -2,18 +2,19 @@ package KondoKit import java.awt.Canvas import java.awt.Color +import java.awt.Dimension import java.awt.Font import java.awt.Graphics class ProgressBar( - private var progress: Double, - private val barColor: Color, - private var currentLevel: Int = 0, - private var nextLevel: Int = 1 + private var progress: Double, + private val barColor: Color, + private var currentLevel: Int = 0, + private var nextLevel: Int = 1 ) : Canvas() { init { - font = Font("Arial", Font.PLAIN, 12) + font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) } override fun paint(g: Graphics) { @@ -25,22 +26,32 @@ class ProgressBar( g.fillRect(0, 0, width, this.height) // Draw the unfilled part of the progress bar - g.color = Color(100, 100, 100) + g.color = Color(61, 56, 49) // from Runelite g.fillRect(width, 0, this.width - width, this.height) + // Variables for text position + val textY = this.height / 2 + 6 + // Draw the current level on the far left - g.color = Color(255, 255, 255) - g.drawString("Lvl. $currentLevel", 5, this.height / 2 + 4) + drawTextWithShadow(g, "Lvl. $currentLevel", 5, textY, Color(255, 255, 255)) // Draw the percentage in the middle val percentageText = String.format("%.2f%%", progress) val percentageWidth = g.fontMetrics.stringWidth(percentageText) - g.drawString(percentageText, (this.width - percentageWidth) / 2, this.height / 2 + 4) + drawTextWithShadow(g, percentageText, (this.width - percentageWidth) / 2, textY, Color(255, 255, 255)) // Draw the next level on the far right val nextLevelText = "Lvl. $nextLevel" val nextLevelWidth = g.fontMetrics.stringWidth(nextLevelText) - g.drawString(nextLevelText, this.width - nextLevelWidth - 5, this.height / 2 + 4) + drawTextWithShadow(g, nextLevelText, this.width - nextLevelWidth - 5, textY, Color(255, 255, 255)) + } + + override fun getPreferredSize(): Dimension { + return Dimension(220, 16) // Force the height to 16px, width can be anything appropriate + } + + override fun getMinimumSize(): Dimension { + return Dimension(220, 16) // Force the minimum height to 16px, width can be smaller } fun updateProgress(newProgress: Double, currentLevel: Int, nextLevel: Int, isVisible : Boolean) { @@ -50,4 +61,15 @@ class ProgressBar( if(isVisible) repaint() } + + // Helper function to draw text with a shadow effect + private fun drawTextWithShadow(g: Graphics, text: String, x: Int, y: Int, textColor: Color) { + // Draw shadow (black text with -1 x and -1 y offset) + g.color = Color(0, 0, 0) + g.drawString(text, x + 1, y + 1) + + // Draw actual text on top + g.color = textColor + g.drawString(text, x, y) + } } diff --git a/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt b/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt index a9227c9..40bad3d 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt @@ -1,35 +1,50 @@ package KondoKit -import KondoKit.KondoKitUtils.convertValue +import KondoKit.Helpers.convertValue +import KondoKit.Helpers.showToast 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.Plugin +import plugin.PluginInfo import plugin.PluginRepository import java.awt.* -import java.lang.reflect.Field +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import java.util.* import java.util.Timer import javax.swing.* +import kotlin.math.ceil + +/* + This is used for the runtime editing of plugin variables. + To expose fields add the @Exposed annotation. + When they are applied this will trigger an invoke of OnKondoValueUpdated() + if it is implemented. Check GroundItems plugin for an example. + */ + object ReflectiveEditorView { - fun createReflectiveEditorView(): JPanel { + var reflectiveEditorView: JPanel? = null + fun createReflectiveEditorView() { val reflectiveEditorPanel = JPanel(BorderLayout()) reflectiveEditorPanel.background = VIEW_BACKGROUND_COLOR reflectiveEditorPanel.add(Box.createVerticalStrut(5)) reflectiveEditorPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) - return reflectiveEditorPanel + reflectiveEditorView = reflectiveEditorPanel + addPlugins(reflectiveEditorView!!) } fun addPlugins(reflectiveEditorView: JPanel) { + reflectiveEditorView.removeAll() // clear previous try { val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins") loadedPluginsField.isAccessible = true val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *> - for ((_, plugin) in loadedPlugins) { - addPluginToEditor(reflectiveEditorView, plugin as Plugin) + for ((pluginInfo, plugin) in loadedPlugins) { + addPluginToEditor(reflectiveEditorView, pluginInfo as PluginInfo, plugin as Plugin) } } catch (e: Exception) { e.printStackTrace() @@ -38,112 +53,214 @@ object ReflectiveEditorView { reflectiveEditorView.repaint() } - private fun addPluginToEditor(reflectiveEditorView: JPanel, plugin: Any) { + private fun addPluginToEditor(reflectiveEditorView: JPanel, pluginInfo : PluginInfo, plugin: Plugin) { reflectiveEditorView.layout = BoxLayout(reflectiveEditorView, BoxLayout.Y_AXIS) - val fieldNotifier = KondoKitUtils.FieldNotifier(plugin) - val exposedFields = KondoKitUtils.getKondoExposedFields(plugin) - - if (exposedFields.isNotEmpty()) { - val packageName = plugin.javaClass.`package`.name - val labelPanel = JPanel(BorderLayout()) - labelPanel.maximumSize = Dimension(Int.MAX_VALUE, 30) // Adjust height to be minimal - labelPanel.background = VIEW_BACKGROUND_COLOR // Ensure it matches the overall background - labelPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0) - - val label = JLabel("$packageName", SwingConstants.CENTER) - label.foreground = primaryColor - label.font = Font("Arial", Font.BOLD, 14) - labelPanel.add(label, BorderLayout.CENTER) - reflectiveEditorView.add(labelPanel) + val fieldNotifier = Helpers.FieldNotifier(plugin) + val exposedFields = plugin.javaClass.declaredFields.filter { field -> + field.annotations.any { annotation -> + annotation.annotationClass.simpleName == "Exposed" + } } - for (field in exposedFields) { - field.isAccessible = true + if (exposedFields.isNotEmpty()) { - val fieldPanel = JPanel() - fieldPanel.layout = GridBagLayout() - fieldPanel.background = WIDGET_COLOR // Match the background for minimal borders - fieldPanel.foreground = secondaryColor - fieldPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0) // No visible border, just spacing - fieldPanel.maximumSize = Dimension(Int.MAX_VALUE, 40) + val packageName = plugin.javaClass.`package`.name + val version = pluginInfo.version + val labelPanel = JPanel(BorderLayout()) + labelPanel.maximumSize = Dimension(Int.MAX_VALUE, 30) + labelPanel.background = VIEW_BACKGROUND_COLOR + labelPanel.border = BorderFactory.createEmptyBorder(5, 0, 0, 0) - val gbc = GridBagConstraints() - gbc.insets = Insets(0, 5, 0, 5) // Less padding, more minimal spacing + val label = JLabel("$packageName v$version", SwingConstants.CENTER) + label.foreground = primaryColor + label.font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) + labelPanel.add(label, BorderLayout.CENTER) + label.isOpaque = true + label.background = Color(21, 21, 21) + reflectiveEditorView.add(labelPanel) - val label = JLabel(field.name.removePrefix(KondoKitUtils.KONDO_PREFIX).capitalize()) - label.foreground = secondaryColor - gbc.gridx = 0 - gbc.gridy = 0 - gbc.weightx = 0.0 - gbc.anchor = GridBagConstraints.WEST - fieldPanel.add(label, gbc) + for (field in exposedFields) { + field.isAccessible = true - val textField = JTextField(field.get(plugin)?.toString() ?: "") - textField.background = VIEW_BACKGROUND_COLOR - textField.foreground = secondaryColor - textField.border = BorderFactory.createLineBorder(WIDGET_COLOR, 1) // Subtle border - gbc.gridx = 1 - gbc.gridy = 0 - gbc.weightx = 1.0 - gbc.fill = GridBagConstraints.HORIZONTAL - fieldPanel.add(textField, gbc) - - val applyButton = JButton("Apply") - applyButton.background = primaryColor - applyButton.foreground = VIEW_BACKGROUND_COLOR - applyButton.border = BorderFactory.createLineBorder(primaryColor, 1) - gbc.gridx = 2 - gbc.gridy = 0 - gbc.weightx = 0.0 - gbc.fill = GridBagConstraints.NONE - applyButton.addActionListener { - try { - val newValue = convertValue(field.type, field.genericType, textField.text) - fieldNotifier.setFieldValue(field, newValue) - JOptionPane.showMessageDialog( - null, - "${field.name.removePrefix(KondoKitUtils.KONDO_PREFIX)} updated successfully!" - ) - } catch (e: Exception) { - JOptionPane.showMessageDialog( - null, - "Failed to update ${field.name.removePrefix(KondoKitUtils.KONDO_PREFIX)}: ${e.message}", - "Error", - JOptionPane.ERROR_MESSAGE - ) + // Get the "Exposed" annotation specifically and retrieve its description, if available + val exposedAnnotation = field.annotations.firstOrNull { annotation -> + annotation.annotationClass.simpleName == "Exposed" } - } - fieldPanel.add(applyButton, gbc) - reflectiveEditorView.add(fieldPanel) + val description = exposedAnnotation?.let { annotation -> + try { + val descriptionField = annotation.annotationClass.java.getMethod("description") + descriptionField.invoke(annotation) as String + } catch (e: NoSuchMethodException) { + "" // No description method, return empty string + } + } ?: "" - var previousValue = field.get(plugin)?.toString() - val timer = Timer() - timer.schedule(object : TimerTask() { - override fun run() { - val currentValue = field.get(plugin)?.toString() - if (currentValue != previousValue) { - previousValue = currentValue - SwingUtilities.invokeLater { - fieldNotifier.notifyFieldChange(field, currentValue) + val fieldPanel = JPanel() + fieldPanel.layout = GridBagLayout() + fieldPanel.background = WIDGET_COLOR + fieldPanel.foreground = secondaryColor + fieldPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0) + fieldPanel.maximumSize = Dimension(Int.MAX_VALUE, 40) + + val gbc = GridBagConstraints() + gbc.insets = Insets(0, 5, 0, 5) + + val label = JLabel(field.name.capitalize()) + label.foreground = secondaryColor + gbc.gridx = 0 + gbc.gridy = 0 + gbc.weightx = 0.0 + label.font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) + gbc.anchor = GridBagConstraints.WEST + fieldPanel.add(label, gbc) + + // Create appropriate input component based on field type + val inputComponent: JComponent = when { + field.type == Boolean::class.javaPrimitiveType || field.type == java.lang.Boolean::class.java -> JCheckBox().apply { + isSelected = field.get(plugin) as Boolean + } + + field.type.isEnum -> JComboBox((field.type.enumConstants as Array>)).apply { + selectedItem = field.get(plugin) + } + + field.type == Int::class.javaPrimitiveType || field.type == Integer::class.java -> JSpinner(SpinnerNumberModel(field.get(plugin) as Int, Int.MIN_VALUE, Int.MAX_VALUE, 1)) + field.type == Float::class.javaPrimitiveType || field.type == Double::class.javaPrimitiveType || field.type == java.lang.Float::class.java || field.type == java.lang.Double::class.java -> JSpinner(SpinnerNumberModel((field.get(plugin) as Number).toDouble(), -Double.MAX_VALUE, Double.MAX_VALUE, 0.1)) + else -> JTextField(field.get(plugin)?.toString() ?: "") + } + + // Add mouse listener to the label only if a description is available + if (description.isNotBlank()) { + label.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + label.addMouseListener(object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent) { + showCustomToolTip(description, label) + } + + override fun mouseExited(e: MouseEvent) { + customToolTipWindow?.isVisible = false + } + }) + } + + gbc.gridx = 1 + gbc.gridy = 0 + gbc.weightx = 1.0 + gbc.fill = GridBagConstraints.HORIZONTAL + fieldPanel.add(inputComponent, gbc) + + val applyButton = JButton("\u2714").apply { + maximumSize = Dimension(Int.MAX_VALUE, 10) + } + gbc.gridx = 2 + gbc.gridy = 0 + gbc.weightx = 0.0 + gbc.fill = GridBagConstraints.NONE + applyButton.addActionListener { + try { + val newValue = when (inputComponent) { + is JCheckBox -> inputComponent.isSelected + is JComboBox<*> -> inputComponent.selectedItem + is JSpinner -> inputComponent.value + is JTextField -> convertValue(field.type, field.genericType, inputComponent.text) + else -> throw IllegalArgumentException("Unsupported input component type") + } + fieldNotifier.setFieldValue(field, newValue) + showToast( + reflectiveEditorView, + "${field.name} updated successfully!" + ) + } catch (e: Exception) { + showToast( + reflectiveEditorView, + "Failed to update ${field.name}: ${e.message}", + JOptionPane.ERROR_MESSAGE + ) + } + } + + fieldPanel.add(applyButton, gbc) + reflectiveEditorView.add(fieldPanel) + + // Track field changes in real-time and update UI + var previousValue = field.get(plugin)?.toString() + val timer = Timer() + timer.schedule(object : TimerTask() { + override fun run() { + val currentValue = field.get(plugin)?.toString() + if (currentValue != previousValue) { + previousValue = currentValue + SwingUtilities.invokeLater { + // Update the inputComponent based on the new value + when (inputComponent) { + is JCheckBox -> inputComponent.isSelected = field.get(plugin) as Boolean + is JComboBox<*> -> inputComponent.selectedItem = field.get(plugin) + is JSpinner -> inputComponent.value = field.get(plugin) + is JTextField -> inputComponent.text = field.get(plugin)?.toString() ?: "" + } + } } } - } - }, 0, 1000) + }, 0, 1000) // Poll every 1000 milliseconds (1 second) + } - fieldNotifier.addObserver(object : KondoKitUtils.FieldObserver { - override fun onFieldChange(field: Field, newValue: Any?) { - if (field.name.removePrefix(KondoKitUtils.KONDO_PREFIX).equals(label.text, ignoreCase = true)) { - textField.text = newValue?.toString() ?: "" - textField.revalidate() - textField.repaint() - } - } - }) - } - if (exposedFields.isNotEmpty()) { - reflectiveEditorView.add(Box.createVerticalStrut(10)) + if (exposedFields.isNotEmpty()) { + reflectiveEditorView.add(Box.createVerticalStrut(5)) + } } + reflectiveEditorView.revalidate() + if(KondoKit.plugin.StateManager.focusedView == "REFLECTIVE_EDITOR_VIEW") + reflectiveEditorView.repaint() } -} + + var customToolTipWindow: JWindow? = null + + fun showCustomToolTip(text: String, component: JComponent) { + val _font = Font("RuneScape Small", Font.PLAIN, 16) + val backgroundColor = Color(50, 50, 50) + val maxWidth = 150 + val lineHeight = 16 + + // Create a dummy JLabel to get FontMetrics for the font used in the tooltip + val dummyLabel = JLabel() + dummyLabel.font = _font + val fontMetrics = dummyLabel.getFontMetrics(_font) + + // Calculate the approximate width of the text + val textWidth = fontMetrics.stringWidth(text) + + // Calculate the number of lines required based on the text width and max tooltip width + val numberOfLines = ceil(textWidth.toDouble() / maxWidth).toInt() + + // Calculate the required height of the tooltip + val requiredHeight = numberOfLines * lineHeight + 6 // Adding some padding + + if (customToolTipWindow == null) { + customToolTipWindow = JWindow().apply { + contentPane = JLabel("
$text
").apply { + border = BorderFactory.createLineBorder(Color.BLACK) + isOpaque = true + background = backgroundColor + foreground = Color.WHITE + font = _font + maximumSize = Dimension(maxWidth, Int.MAX_VALUE) + preferredSize = Dimension(maxWidth, requiredHeight) + } + pack() + } + } else { + // Update the tooltip text + val label = customToolTipWindow!!.contentPane as JLabel + label.text = "
$text
" + label.preferredSize = Dimension(maxWidth, requiredHeight) + customToolTipWindow!!.pack() + } + + // Position the tooltip near the component + val locationOnScreen = component.locationOnScreen + customToolTipWindow!!.setLocation(locationOnScreen.x, locationOnScreen.y + 15) + customToolTipWindow!!.isVisible = true + } +} \ No newline at end of file diff --git a/plugin-playground/src/main/kotlin/KondoKit/ScrollablePanel.kt b/plugin-playground/src/main/kotlin/KondoKit/ScrollablePanel.kt new file mode 100644 index 0000000..44467a7 --- /dev/null +++ b/plugin-playground/src/main/kotlin/KondoKit/ScrollablePanel.kt @@ -0,0 +1,154 @@ +package KondoKit + +import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR +import rt4.GameShell.frame +import java.awt.Color +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.Rectangle +import java.awt.event.* +import java.util.* +import javax.swing.JPanel + +class ScrollablePanel(private val content: JPanel) : JPanel() { + private var lastMouseY = 0 + private var currentOffsetY = 0 + private var scrollbarHeight = 0 + private var scrollbarY = 0 + private var showScrollbar = false + private var draggingScrollPill = false + + // Define a buffer for the view height (extra space for smoother scrolling) + private val viewBuffer = -30 + + init { + layout = null + background = VIEW_BACKGROUND_COLOR // Color.red color can be set to debug + + // Initial content bounds + content.bounds = Rectangle(0, 0, 242, content.preferredSize.height.coerceAtLeast(frame.height + viewBuffer)) + add(content) + + // Add listeners for scrolling interactions + addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) { + lastMouseY = e.y + if (showScrollbar && e.x in (242 - 10)..242 && e.y in scrollbarY..(scrollbarY + scrollbarHeight)) { + draggingScrollPill = true + } + } + + override fun mouseReleased(e: MouseEvent) { + draggingScrollPill = false + } + }) + + addMouseMotionListener(object : MouseMotionAdapter() { + override fun mouseDragged(e: MouseEvent) { + val deltaY = e.y - lastMouseY + if (draggingScrollPill && showScrollbar) { + val viewHeight = frame.height + val contentHeight = content.height + val scrollRatio = contentHeight.toDouble() / viewHeight + scrollContent((deltaY * scrollRatio).toInt()) + } else if (showScrollbar) { + scrollContent(deltaY) + } + lastMouseY = e.y + } + }) + + addMouseWheelListener { e -> + if (showScrollbar) { + scrollContent(-e.wheelRotation * 20) + } + } + + // Timer to periodically check and update scrollbar status + Timer().schedule(object : TimerTask() { + override fun run() { + updateScrollbar() + } + }, 0, 1000) + + // Component listener for resizing the frame + frame.addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent) { + handleResize() + } + }) + } + + private fun handleResize() { + // Ensure the ScrollablePanel resizes with the frame + bounds = Rectangle(0, 0, 242, frame.height) + + // Dynamically update content bounds and scrollbar on frame resize with buffer + content.bounds = Rectangle(0, 0, 242, content.preferredSize.height.coerceAtLeast(frame.height + viewBuffer)) + showScrollbar = content.height > frame.height + + currentOffsetY = 0 + + content.setLocation(0, currentOffsetY) + updateScrollbar() + + revalidate() + repaint() + } + + private fun scrollContent(deltaY: Int) { + if (!showScrollbar) { + currentOffsetY = 0 + content.setLocation(0, currentOffsetY) + return + } + + currentOffsetY += deltaY + + // Apply buffer to maxOffset + val maxOffset = (frame.height - content.height + viewBuffer).coerceAtMost(0) + currentOffsetY = currentOffsetY.coerceAtMost(0).coerceAtLeast(maxOffset) + + content.setLocation(0, currentOffsetY) + + val contentHeight = content.height + val viewHeight = frame.height + viewBuffer + val scrollableRatio = viewHeight.toDouble() / contentHeight + scrollbarY = ((-currentOffsetY / contentHeight.toDouble()) * viewHeight).toInt() + scrollbarHeight = (viewHeight * scrollableRatio).toInt().coerceAtLeast(20) + + repaint() + } + + private fun updateScrollbar() { + showScrollbar = content.height > frame.height + + val contentHeight = content.height + val viewHeight = frame.height + viewBuffer + + if (showScrollbar) { + val scrollableRatio = viewHeight.toDouble() / contentHeight + scrollbarY = ((-currentOffsetY / contentHeight.toDouble()) * viewHeight).toInt() + scrollbarHeight = (viewHeight * scrollableRatio).toInt().coerceAtLeast(20) + } else { + scrollbarY = 0 + scrollbarHeight = 0 + } + + repaint() + } + + override fun paintComponent(g: Graphics) { + super.paintComponent(g) + } + + override fun paintChildren(g: Graphics) { + super.paintChildren(g) + if (showScrollbar) { + val g2 = g as Graphics2D + val scrollbarX = 238 + g2.color = Color(64, 64, 64) + g2.fillRect(scrollbarX, scrollbarY, 2, scrollbarHeight) + } + } +} diff --git a/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt b/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt index 0472ce9..ff41e11 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt @@ -4,32 +4,36 @@ import KondoKit.Helpers.formatHtmlLabelText import KondoKit.Helpers.formatNumber import KondoKit.Helpers.getProgressBarColor import KondoKit.Helpers.getSpriteId +import KondoKit.Helpers.addMouseListenerToAll import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.plugin.Companion.IMAGE_SIZE +import KondoKit.plugin.Companion.LVL_ICON 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.WIDGET_SIZE -import KondoKit.plugin.Companion.kondoExposed_playerXPMultiplier +import KondoKit.plugin.Companion.playerXPMultiplier import KondoKit.plugin.Companion.primaryColor import KondoKit.plugin.Companion.secondaryColor -import KondoKit.plugin.StateManager.totalXPWidget import plugin.api.API import java.awt.* +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import java.io.BufferedReader import java.io.InputStreamReader import java.nio.charset.StandardCharsets -import javax.swing.Box -import javax.swing.BoxLayout -import javax.swing.JLabel -import javax.swing.JPanel +import javax.swing.* object XPTrackerView { - private val COMBAT_SKILLS = intArrayOf(0,1,2,3,4) + val xpWidgets: MutableMap = HashMap() + var totalXPWidget: XPWidget? = null + val initialXP: MutableMap = HashMap() + var xpTrackerView: JPanel? = null + val npcHitpointsMap: Map = try { - BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("npc_hitpoints_map.json"), StandardCharsets.UTF_8)) + BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/npc_hitpoints_map.json"), StandardCharsets.UTF_8)) .useLines { lines -> val json = lines.joinToString("\n") val pairs = json.trim().removeSurrounding("{", "}").split(",") @@ -74,8 +78,8 @@ object XPTrackerView { if(LootTrackerView.lastConfirmedKillNpcId != -1 && npcHitpointsMap.isNotEmpty()) { val npcHP = npcHitpointsMap[LootTrackerView.lastConfirmedKillNpcId] val xpPerKill = when (xpWidget.skillId) { - 3 -> kondoExposed_playerXPMultiplier * (npcHP ?: 1) // Hitpoints - else -> kondoExposed_playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills + 3 -> playerXPMultiplier * (npcHP ?: 1) // Hitpoints + else -> playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills } val remainingKills = xpLeft / xpPerKill xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Kills: ", primaryColor, remainingKills.toString(), secondaryColor) @@ -96,20 +100,57 @@ object XPTrackerView { xpWidget.previousXp = xp if (plugin.StateManager.focusedView == "XP_TRACKER_VIEW") - xpWidget.panel.repaint() + xpWidget.container.repaint() } private fun updateTotalXPWidget(xpGainedSinceLastUpdate: Int) { - val totalXPWidget = plugin.StateManager.totalXPWidget ?: return + val totalXPWidget = totalXPWidget ?: return totalXPWidget.totalXpGained += xpGainedSinceLastUpdate val formattedXp = formatNumber(totalXPWidget.totalXpGained) totalXPWidget.xpGainedLabel.text = formatHtmlLabelText("Gained: ", primaryColor, formattedXp, secondaryColor) if (plugin.StateManager.focusedView == "XP_TRACKER_VIEW") - totalXPWidget.panel.repaint() + totalXPWidget.container.repaint() } + fun resetXPTracker(xpTrackerView : JPanel){ + + // Redo logic here + xpTrackerView.removeAll() + val popupMenu = createResetMenu() + + // 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) + } + } + } + + // Create the XP widget + totalXPWidget = createTotalXPWidget() + val wrapped = wrappedWidget(totalXPWidget!!.container) + addMouseListenerToAll(wrapped,rightClickListener) + wrapped.addMouseListener(rightClickListener) + xpTrackerView.add(Box.createVerticalStrut(5)) + xpTrackerView.add(wrapped) + xpTrackerView.add(Box.createVerticalStrut(5)) + + initialXP.clear() + xpWidgets.clear() + + xpTrackerView.revalidate() + if (plugin.StateManager.focusedView == "XP_TRACKER_VIEW") + xpTrackerView.repaint() + } fun createTotalXPWidget(): XPWidget { val widgetPanel = Panel().apply { @@ -120,11 +161,9 @@ object XPTrackerView { minimumSize = TOTAL_XP_WIDGET_SIZE } - val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(898)) - + val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(LVL_ICON)) val imageContainer = Panel(FlowLayout()).apply { - background = WIDGET_COLOR preferredSize = IMAGE_SIZE maximumSize = IMAGE_SIZE minimumSize = IMAGE_SIZE @@ -133,15 +172,14 @@ object XPTrackerView { bufferedImageSprite.let { image -> val imageCanvas = ImageCanvas(image).apply { - background = WIDGET_COLOR - preferredSize = Dimension(image.width, image.height) - maximumSize = Dimension(image.width, image.height) - minimumSize = Dimension(image.width, image.height) - size = Dimension(image.width, image.height) + preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height) + maximumSize = preferredSize + minimumSize = preferredSize + size = preferredSize } imageContainer.add(imageCanvas) - imageContainer.size = Dimension(image.width, image.height) + imageContainer.size = Dimension(bufferedImageSprite.width, bufferedImageSprite.height) imageContainer.revalidate() if(plugin.StateManager.focusedView == "XP_TRACKER_VIEW") @@ -149,14 +187,13 @@ object XPTrackerView { } val textPanel = Panel().apply { - layout = GridLayout(2, 1, 5, 5) - background = WIDGET_COLOR + layout = GridLayout(2, 1, 5, 0) } - val font = Font("Arial", Font.PLAIN, 11) + val font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) val xpGainedLabel = JLabel( - formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor) + formatHtmlLabelText("Gained: ", primaryColor, "0", secondaryColor) ).apply { this.horizontalAlignment = JLabel.LEFT this.font = font @@ -177,7 +214,7 @@ object XPTrackerView { return XPWidget( skillId = -1, - panel = widgetPanel, + container = widgetPanel, xpGainedLabel = xpGainedLabel, xpLeftLabel = JLabel(formatHtmlLabelText("XP Left: ", primaryColor, "0", secondaryColor)).apply { this.horizontalAlignment = JLabel.LEFT @@ -193,19 +230,83 @@ object XPTrackerView { } - fun createXPTrackerView(): JPanel? { - val widgetViewPanel = JPanel() - widgetViewPanel.layout = BoxLayout(widgetViewPanel, BoxLayout.Y_AXIS) - widgetViewPanel.background = VIEW_BACKGROUND_COLOR - widgetViewPanel.add(Box.createVerticalStrut(5)) + fun createXPTrackerView(){ + val widgetViewPanel = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + background = VIEW_BACKGROUND_COLOR + } + val popupMenu = createResetMenu() + + // 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) + } + } + } + + // Create the XP widget totalXPWidget = createTotalXPWidget() - widgetViewPanel.add(wrappedWidget(totalXPWidget!!.panel)) + val wrapped = wrappedWidget(totalXPWidget!!.container) + addMouseListenerToAll(wrapped,rightClickListener) + wrapped.addMouseListener(rightClickListener) + widgetViewPanel.add(Box.createVerticalStrut(5)) + widgetViewPanel.add(wrapped) widgetViewPanel.add(Box.createVerticalStrut(5)) - return widgetViewPanel + xpTrackerView = widgetViewPanel } + + fun createResetMenu(): 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 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 + } + + val menuItem2 = JMenuItem("Option 2").apply { + font = rFont + background = Color(45, 45, 45) + foreground = Color(220, 220, 220) + } + + val menuItem3 = JMenuItem("Option 3").apply { + font = rFont + background = Color(45, 45, 45) + foreground = Color(220, 220, 220) + } + + // Add menu items to the popup menu + popupMenu.add(menuItem1) + //popupMenu.add(menuItem2) + //popupMenu.add(menuItem3) + + // Add action listeners to each menu item (optional) + menuItem1.addActionListener { resetXPTracker(xpTrackerView!!) } + //menuItem2.addActionListener { println("Option 2 selected") } + //menuItem3.addActionListener { println("Option 3 selected") } + + return popupMenu + } + + fun createXPWidget(skillId: Int, previousXp: Int): XPWidget { val widgetPanel = Panel().apply { layout = BorderLayout(5, 5) @@ -242,14 +343,14 @@ object XPTrackerView { } val textPanel = Panel().apply { - layout = GridLayout(2, 2, 5, 5) + layout = GridLayout(2, 2, 5, 0) background = WIDGET_COLOR } - val font = Font("Arial", Font.PLAIN, 11) + val font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) val xpGainedLabel = JLabel( - formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor) + formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor) ).apply { this.horizontalAlignment = JLabel.LEFT this.font = font @@ -302,7 +403,7 @@ object XPTrackerView { return XPWidget( skillId = skillId, - panel = widgetPanel, + container = widgetPanel, xpGainedLabel = xpGainedLabel, xpLeftLabel = xpLeftLabel, xpPerHourLabel = xpPerHourLabel, @@ -314,18 +415,18 @@ object XPTrackerView { ) } - fun wrappedWidget(component: Component, padding: Int = 7): Panel { + fun wrappedWidget(component: Component, padding: Int = 7): Container { val outerPanelSize = Dimension( - component.preferredSize.width + 2 * padding, - component.preferredSize.height + 2 * padding + component.preferredSize.width + 2 * padding, + component.preferredSize.height + 2 * padding ) - val outerPanel = Panel(GridBagLayout()).apply { + val outerPanel = JPanel(GridBagLayout()).apply { background = WIDGET_COLOR preferredSize = outerPanelSize maximumSize = outerPanelSize minimumSize = outerPanelSize } - val innerPanel = Panel(BorderLayout()).apply { + val innerPanel = JPanel(BorderLayout()).apply { background = WIDGET_COLOR preferredSize = component.preferredSize maximumSize = component.preferredSize @@ -343,14 +444,14 @@ object XPTrackerView { data class XPWidget( - val panel: Panel, - val skillId: Int, - val xpGainedLabel: JLabel, - val xpLeftLabel: JLabel, - val xpPerHourLabel: JLabel, - val actionsRemainingLabel: JLabel, - val progressBar: ProgressBar, - var totalXpGained: Int = 0, - var startTime: Long = System.currentTimeMillis(), - var previousXp: Int = 0 + val container: Container, + val skillId: Int, + val xpGainedLabel: JLabel, + val xpLeftLabel: JLabel, + val xpPerHourLabel: JLabel, + val actionsRemainingLabel: JLabel, + val progressBar: ProgressBar, + var totalXpGained: Int = 0, + var startTime: Long = System.currentTimeMillis(), + var previousXp: Int = 0 ) \ No newline at end of file diff --git a/plugin-playground/src/main/kotlin/KondoKit/plugin.kt b/plugin-playground/src/main/kotlin/KondoKit/plugin.kt index 65de95e..e8233f9 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/plugin.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/plugin.kt @@ -5,22 +5,27 @@ import KondoKit.Helpers.formatHtmlLabelText import KondoKit.Helpers.formatNumber import KondoKit.Helpers.getSpriteId import KondoKit.HiscoresView.createHiscoreSearchView +import KondoKit.HiscoresView.hiScoreView import KondoKit.LootTrackerView.BAG_ICON import KondoKit.LootTrackerView.createLootTrackerView +import KondoKit.LootTrackerView.lootTrackerView import KondoKit.LootTrackerView.npcDeathSnapshots import KondoKit.LootTrackerView.onPostClientTick import KondoKit.LootTrackerView.takeGroundSnapshot import KondoKit.ReflectiveEditorView.addPlugins import KondoKit.ReflectiveEditorView.createReflectiveEditorView +import KondoKit.ReflectiveEditorView.reflectiveEditorView import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite -import KondoKit.XPTrackerView.createTotalXPWidget import KondoKit.XPTrackerView.createXPTrackerView import KondoKit.XPTrackerView.createXPWidget +import KondoKit.XPTrackerView.initialXP +import KondoKit.XPTrackerView.resetXPTracker +import KondoKit.XPTrackerView.totalXPWidget import KondoKit.XPTrackerView.updateWidget import KondoKit.XPTrackerView.wrappedWidget -import KondoKit.plugin.StateManager.initialXP -import KondoKit.plugin.StateManager.totalXPWidget -import KondoKit.plugin.StateManager.xpWidgets +import KondoKit.XPTrackerView.xpTrackerView +import KondoKit.XPTrackerView.xpWidgets +import KondoKit.plugin.StateManager.focusedView import plugin.Plugin import plugin.api.* import plugin.api.API.* @@ -38,37 +43,47 @@ import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.* +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Exposed(val description: String = "") class plugin : Plugin() { companion object { - val WIDGET_SIZE = Dimension(270, 55) - val TOTAL_XP_WIDGET_SIZE = Dimension(270, 30) + val WIDGET_SIZE = Dimension(220, 50) + val TOTAL_XP_WIDGET_SIZE = Dimension(220, 30) val IMAGE_SIZE = Dimension(20, 20) - val WIDGET_COLOR = Color(27, 27, 27) - val VIEW_BACKGROUND_COLOR = Color(37, 37, 37) - val primaryColor = Color(129, 129, 129) // Color for "XP Gained:" - val secondaryColor = Color(226, 226, 226) // Color for "0" - var kondoExposed_useLiveGEPrices = true - var kondoExposed_playerXPMultiplier = 5 - const val FIXED_WIDTH = 782 - const val SCROLLPANE_WIDTH = 340 + val WIDGET_COLOR = Color(30, 30, 30) + val VIEW_BACKGROUND_COLOR = Color(40, 40, 40) + val primaryColor = Color(165, 165, 165) // Color for "XP Gained:" + val secondaryColor = Color(255, 255, 255) // Color for "0" + + @Exposed(description = "Default: true, Use Local JSON or the prices from the Live/Stable server API") + var useLiveGEPrices = true + + @Exposed(description = "Used to calculate Combat Actions until next level.") + var playerXPMultiplier = 5 + + @Exposed(description = "Start minimized/collapsed by default") + var launchMinimized = false + + private const val FIXED_WIDTH = 782 + private const val NAVBAR_WIDTH = 30 + private const val MAIN_CONTENT_WIDTH = 242 private const val WRENCH_ICON = 907 - private const val LVL_ICON = 898 private const val LOOT_ICON = 777 private const val MAG_SPRITE = 1423 + const val LVL_ICON = 898 private lateinit var cardLayout: CardLayout - private lateinit var mainContentPanel: Panel - private var scrollPane: JScrollPane? = null - private var hiScoreView: JPanel? = null - private var reflectiveEditorView: JPanel? = null - private var lootTrackerView: JPanel? = null - private var xpTrackerView: JPanel? = null + private lateinit var mainContentPanel: JPanel + private var rightPanelWrapper: JScrollPane? = null private var accumulatedTime = 0L + private var reloadInterfaces = false private const val tickInterval = 600L private var pluginsReloaded = false - private var loginScreen = 160; + private var loginScreen = 160 private var lastLogin = "" private var initialized = false; + private var lastClickTime = 0L } fun allSpritesLoaded() : Boolean { @@ -95,43 +110,36 @@ class plugin : Plugin() { if (lastLogin != "" && lastLogin != Player.usernameInput.toString()) { // if we logged in with a new character // we need to reset the trackers - xpTrackerView?.removeAll() - totalXPWidget = createTotalXPWidget() - xpTrackerView?.add(Box.createVerticalStrut(5)) - xpTrackerView?.add(wrappedWidget(totalXPWidget!!.panel)) - xpTrackerView?.add(Box.createVerticalStrut(5)) - initialXP.clear() - xpWidgets.clear() - - xpTrackerView?.revalidate() - if (StateManager.focusedView == "XP_TRACKER_VIEW") - xpTrackerView?.repaint() + xpTrackerView?.let { resetXPTracker(it) } } lastLogin = Player.usernameInput.toString() } private fun UpdateDisplaySettings() { val mode = GetWindowMode() + val currentScrollPaneWidth = if (mainContentPanel.isVisible) NAVBAR_WIDTH + MAIN_CONTENT_WIDTH else NAVBAR_WIDTH when (mode) { WindowMode.FIXED -> { - if (frame.width < FIXED_WIDTH + SCROLLPANE_WIDTH) { - frame.setSize(FIXED_WIDTH + SCROLLPANE_WIDTH, frame.height) + if (frame.width < FIXED_WIDTH + currentScrollPaneWidth) { + frame.setSize(FIXED_WIDTH + currentScrollPaneWidth, frame.height) } - val difference = frame.width - (FIXED_WIDTH + SCROLLPANE_WIDTH) + val difference = frame.width - (FIXED_WIDTH + currentScrollPaneWidth) GameShell.leftMargin = difference / 2 } WindowMode.RESIZABLE -> { - GameShell.canvasWidth -= SCROLLPANE_WIDTH + GameShell.canvasWidth = frame.width - (currentScrollPaneWidth + 16) } } - scrollPane?.revalidate() - scrollPane?.repaint() + rightPanelWrapper?.preferredSize = Dimension(currentScrollPaneWidth, frame.height) + rightPanelWrapper?.revalidate() + rightPanelWrapper?.repaint() } fun OnKondoValueUpdated(){ - StoreData("kondoUseRemoteGE", kondoExposed_useLiveGEPrices) - StoreData("kondoPlayerXPMultiplier", kondoExposed_playerXPMultiplier) + StoreData("kondoUseRemoteGE", useLiveGEPrices) + StoreData("kondoPlayerXPMultiplier", playerXPMultiplier) LootTrackerView.gePriceMap = LootTrackerView.loadGEPrices() + StoreData("kondoLaunchMinimized", launchMinimized) } override fun OnMiniMenuCreate(currentEntries: Array?) { @@ -139,12 +147,16 @@ class plugin : Plugin() { for ((index, entry) in currentEntries.withIndex()) { if (entry.type == MiniMenuType.PLAYER && index == currentEntries.size - 1) { val input = entry.subject - val username = input - .replace(Regex(""), "") - .replace(Regex(""), "") - .split(" ") // Split by spaces - .first() // Take the first part, which is the username - InsertMiniMenuEntry("Lookup", entry.subject, searchHiscore(username.replace(" ","_"))) + // Trim spaces, clean up tags, and remove the level info + val cleanedInput = input + .trim() // Remove any leading/trailing spaces + .replace(Regex(""), "") // Remove color tags + .replace(Regex(""), "") // Remove image tags + .replace(Regex("\\(level: \\d+\\)"), "") // Remove level text e.g. (level: 44) + .trim() // Trim again to remove extra spaces after removing level text + + // Proceed with the full cleaned username + InsertMiniMenuEntry("Lookup", entry.subject, searchHiscore(cleanedInput)) } } } @@ -152,40 +164,31 @@ class plugin : Plugin() { private fun searchHiscore(username: String): Runnable { return Runnable { - cardLayout.show(mainContentPanel, "HISCORE_SEARCH_VIEW") - StateManager.focusedView = "HISCORE_SEARCH_VIEW" + setActiveView("HISCORE_SEARCH_VIEW") val customSearchField = hiScoreView?.let { HiscoresView.CustomSearchField(it) } customSearchField?.searchPlayer(username) ?: run { println("searchView is null or CustomSearchField creation failed.") } - hiScoreView?.repaint() } } - override fun OnPluginsReloaded(): Boolean { if (!initialized) return true UpdateDisplaySettings() - frame.remove(scrollPane) + frame.remove(rightPanelWrapper) frame.layout = BorderLayout() - frame.add(scrollPane, BorderLayout.EAST) - - // Clear or regenerate the reflectiveEditorView - reflectiveEditorView?.removeAll() - reflectiveEditorView?.revalidate() - if(StateManager.focusedView == "REFLECTIVE_EDITOR_VIEW") - reflectiveEditorView?.repaint() + frame.add(rightPanelWrapper, BorderLayout.EAST) frame.revalidate() frame.repaint() pluginsReloaded = true + reloadInterfaces = true return true } - override fun OnXPUpdate(skillId: Int, xp: Int) { if (!initialXP.containsKey(skillId)) { initialXP[skillId] = xp @@ -203,11 +206,11 @@ class plugin : Plugin() { xpWidget = createXPWidget(skillId, previousXp) xpWidgets[skillId] = xpWidget - xpTrackerView?.add(wrappedWidget(xpWidget.panel)) + xpTrackerView?.add(wrappedWidget(xpWidget.container)) xpTrackerView?.add(Box.createVerticalStrut(5)) xpTrackerView?.revalidate() - if(StateManager.focusedView == "XP_TRACKER_VIEW") + if(focusedView == "XP_TRACKER_VIEW") xpTrackerView?.repaint() updateWidget(xpWidget, xp) @@ -221,23 +224,25 @@ class plugin : Plugin() { } if (pluginsReloaded) { - InterfaceList.method3712(true) // Gets the resize working correctly reflectiveEditorView?.let { addPlugins(it) } pluginsReloaded = false } + if (reloadInterfaces){ + InterfaceList.method3712(true) // Gets the resize working correctly + reloadInterfaces = false + } + accumulatedTime += timeDelta if (accumulatedTime >= tickInterval) { lootTrackerView?.let { onPostClientTick(it) } accumulatedTime = 0L } - // Init in the draw call so we know we are between glBegin and glEnd for HD if(!initialized && mainLoadState >= loginScreen) { initKondoUI() } - } private fun initKondoUI(){ @@ -245,25 +250,66 @@ class plugin : Plugin() { if(!allSpritesLoaded()) return; val frame: Frame? = GameShell.frame if (frame != null) { - kondoExposed_useLiveGEPrices = (GetData("kondoUseRemoteGE") as? Boolean) ?: true - kondoExposed_playerXPMultiplier = (GetData("kondoPlayerXPMultiplier") as? Int) ?: 5 - cardLayout = CardLayout() - mainContentPanel = Panel(cardLayout) - mainContentPanel.background = VIEW_BACKGROUND_COLOR - xpTrackerView = createXPTrackerView() - hiScoreView = createHiscoreSearchView() - lootTrackerView = createLootTrackerView() - reflectiveEditorView = createReflectiveEditorView() - mainContentPanel.add(xpTrackerView, "XP_TRACKER_VIEW") - mainContentPanel.add(hiScoreView, "HISCORE_SEARCH_VIEW") - mainContentPanel.add(lootTrackerView, "LOOT_TRACKER_VIEW") - mainContentPanel.add(reflectiveEditorView, "REFLECTIVE_EDITOR_VIEW") + // Disable Font AA + System.setProperty("awt.useSystemAAFontSettings", "off") + System.setProperty("swing.aatext", "false") + + loadFont() + + try { + UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel") + + // Modify the UI properties for a dark theme + UIManager.put("control", Color(50, 50, 50)) // Default background for most controls + UIManager.put("info", Color(50, 50, 50)) + UIManager.put("nimbusBase", Color(35, 35, 35)) // Base color for Nimbus L&F + UIManager.put("nimbusAlertYellow", Color(255, 220, 35)) + UIManager.put("nimbusDisabledText", Color(100, 100, 100)) + UIManager.put("nimbusFocus", Color(115, 164, 209)) + UIManager.put("nimbusGreen", Color(176, 179, 50)) + UIManager.put("nimbusInfoBlue", Color(66, 139, 221)) + UIManager.put("nimbusLightBackground", Color(35, 35, 35)) // Background of text fields, etc. + UIManager.put("nimbusOrange", Color(191, 98, 4)) + UIManager.put("nimbusRed", Color(169, 46, 34)) + UIManager.put("nimbusSelectedText", Color(255, 255, 255)) + UIManager.put("nimbusSelectionBackground", Color(75, 110, 175)) // Selection background + UIManager.put("text", Color(230, 230, 230)) // General text color + + // Update component tree UI to apply the new theme + SwingUtilities.updateComponentTreeUI(GameShell.frame) + } catch (e : Exception) { + e.printStackTrace() + } + + // Restore saved values + useLiveGEPrices = (GetData("kondoUseRemoteGE") as? Boolean) ?: true + playerXPMultiplier = (GetData("kondoPlayerXPMultiplier") as? Int) ?: 5 + launchMinimized = (GetData("kondoLaunchMinimized") as? Boolean) ?: false + + cardLayout = CardLayout() + mainContentPanel = JPanel(cardLayout).apply { + border = BorderFactory.createEmptyBorder(0, 0, 0, 0) // Removes any default border or padding + background = VIEW_BACKGROUND_COLOR + preferredSize = Dimension(MAIN_CONTENT_WIDTH, frame.height) + isOpaque = true + } + + // Register Views + createXPTrackerView() + createHiscoreSearchView() + createLootTrackerView() + createReflectiveEditorView() + + mainContentPanel.add(ScrollablePanel(xpTrackerView!!), "XP_TRACKER_VIEW") + mainContentPanel.add(ScrollablePanel(hiScoreView!!), "HISCORE_SEARCH_VIEW") + mainContentPanel.add(ScrollablePanel(lootTrackerView!!), "LOOT_TRACKER_VIEW") + mainContentPanel.add(ScrollablePanel(reflectiveEditorView!!), "REFLECTIVE_EDITOR_VIEW") val navPanel = Panel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) background = WIDGET_COLOR - preferredSize = Dimension(42, frame.height) + preferredSize = Dimension(NAVBAR_WIDTH, frame.height) } navPanel.add(createNavButton(LVL_ICON, "XP_TRACKER_VIEW")) @@ -271,13 +317,13 @@ class plugin : Plugin() { navPanel.add(createNavButton(LOOT_ICON, "LOOT_TRACKER_VIEW")) navPanel.add(createNavButton(WRENCH_ICON, "REFLECTIVE_EDITOR_VIEW")) - val rightPanel = Panel(BorderLayout()).apply { + var rightPanel = Panel(BorderLayout()).apply { add(mainContentPanel, BorderLayout.CENTER) add(navPanel, BorderLayout.EAST) } - scrollPane = JScrollPane(rightPanel).apply { - preferredSize = Dimension(SCROLLPANE_WIDTH, frame.height) + rightPanelWrapper = JScrollPane(rightPanel).apply { + preferredSize = Dimension(NAVBAR_WIDTH + MAIN_CONTENT_WIDTH, frame.height) background = VIEW_BACKGROUND_COLOR border = BorderFactory.createEmptyBorder() // Removes the border completely horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER @@ -285,91 +331,173 @@ class plugin : Plugin() { } frame.layout = BorderLayout() - scrollPane?.let { frame.add(it, BorderLayout.EAST) } + rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) } - frame.revalidate() - frame.repaint() - - StateManager.focusedView = "XP_TRACKER_VIEW" + if(!launchMinimized){ + setActiveView("XP_TRACKER_VIEW") + } else { + setActiveView("HIDDEN") + } initialized = true pluginsReloaded = true - UpdateDisplaySettings() } } override fun Update() { - xpWidgets.values.forEach { xpWidget -> + + val widgets = xpWidgets.values + val totalXP = totalXPWidget + + widgets.forEach { xpWidget -> val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0 val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0 val formattedXpPerHour = formatNumber(xpPerHour) xpWidget.xpPerHourLabel.text = - formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor) - xpWidget.panel.repaint() + formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor) + xpWidget.container.repaint() } - totalXPWidget?.let { totalXPWidget -> + totalXP?.let { totalXPWidget -> val elapsedTime = (System.currentTimeMillis() - totalXPWidget.startTime) / 1000.0 / 60.0 / 60.0 val totalXPPerHour = if (elapsedTime > 0) (totalXPWidget.totalXpGained / elapsedTime).toInt() else 0 val formattedTotalXpPerHour = formatNumber(totalXPPerHour) totalXPWidget.xpPerHourLabel.text = - formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor) - totalXPWidget.panel.repaint() + formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor) + totalXPWidget.container.repaint() } } - override fun OnKillingBlowNPC(npcID: Int, x: Int, z: Int) { val preDeathSnapshot = takeGroundSnapshot(Pair(x,z)) npcDeathSnapshots[npcID] = LootTrackerView.GroundSnapshot(preDeathSnapshot, Pair(x, z), 0) } - - private fun createNavButton(spriteId: Int, viewName: String): JButton { - val bufferedImageSprite = getBufferedImageFromSprite(GetSprite(spriteId)) - val buttonSize = Dimension(42, 42) - val imageSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height) - - val actionListener = ActionListener { + private fun setActiveView(viewName: String) { + // Handle the visibility of the main content panel + if (viewName == "HIDDEN") { + mainContentPanel.isVisible = false + } else { + if (!mainContentPanel.isVisible) { + mainContentPanel.isVisible = true + } cardLayout.show(mainContentPanel, viewName) - StateManager.focusedView = viewName } + reloadInterfaces = true + UpdateDisplaySettings() + + // Revalidate and repaint necessary panels + mainContentPanel.revalidate() + mainContentPanel.repaint() + rightPanelWrapper?.revalidate() + rightPanelWrapper?.repaint() + frame?.revalidate() + frame?.repaint() + + focusedView = viewName + } + + private fun createNavButton(spriteId: Int, viewName: String): JPanel { + val bufferedImageSprite = getBufferedImageFromSprite(GetSprite(spriteId)) + val buttonSize = Dimension(NAVBAR_WIDTH, 32) + val imageSize = Dimension((bufferedImageSprite.width / 1.2f).toInt(), (bufferedImageSprite.height / 1.2f).toInt()) + val cooldownDuration = 100L + + val actionListener = ActionListener { + val currentTime = System.currentTimeMillis() + if (currentTime - lastClickTime < cooldownDuration) { + return@ActionListener + } + lastClickTime = currentTime + + if (focusedView == viewName) { + setActiveView("HIDDEN") + } else { + setActiveView(viewName) + } + } + + // ImageCanvas with forced size val imageCanvas = ImageCanvas(bufferedImageSprite).apply { background = WIDGET_COLOR preferredSize = imageSize maximumSize = imageSize minimumSize = imageSize - addMouseListener(object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent?) { - actionListener.actionPerformed(null) - } - }) } - val button = JButton().apply { + // Wrapping the ImageCanvas in another JPanel to prevent stretching + val imageCanvasWrapper = JPanel().apply { + layout = GridBagLayout() // Keeps the layout of the wrapped panel minimal + preferredSize = imageSize + maximumSize = imageSize + minimumSize = imageSize + isOpaque = false // No background for the wrapper + add(imageCanvas) // Adding ImageCanvas directly, layout won't stretch it + } + + val panelButton = JPanel().apply { layout = GridBagLayout() preferredSize = buttonSize maximumSize = buttonSize minimumSize = buttonSize background = WIDGET_COLOR - isFocusPainted = false - isBorderPainted = false + isOpaque = true // Ensure background is painted val gbc = GridBagConstraints().apply { anchor = GridBagConstraints.CENTER + fill = GridBagConstraints.NONE // Prevents stretching } - add(imageCanvas, gbc) - addActionListener(actionListener) + add(imageCanvasWrapper, gbc) + + // Hover and click behavior + val hoverListener = object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent?) { + background = WIDGET_COLOR.darker() + imageCanvas.fillColor = WIDGET_COLOR.darker() + imageCanvas.repaint() + repaint() + } + + override fun mouseExited(e: MouseEvent?) { + background = WIDGET_COLOR + imageCanvas.fillColor = WIDGET_COLOR + imageCanvas.repaint() + repaint() + } + + override fun mouseClicked(e: MouseEvent?) { + actionListener.actionPerformed(null) + } + } + + addMouseListener(hoverListener) + imageCanvas.addMouseListener(hoverListener) } - return button + return panelButton + } + + + fun loadFont(): Font? { + val fontStream = plugin::class.java.getResourceAsStream("res/runescape_small.ttf") + return if (fontStream != null) { + try { + val font = Font.createFont(Font.TRUETYPE_FONT, fontStream) + val ge = GraphicsEnvironment.getLocalGraphicsEnvironment() + ge.registerFont(font) // Register the font in the graphics environment + font + } catch (e: Exception) { + e.printStackTrace() + null + } + } else { + println("Font not found!") + null + } } object StateManager { - val initialXP: MutableMap = HashMap() - val xpWidgets: MutableMap = HashMap() - var totalXPWidget: XPWidget? = null var focusedView: String = "" } } diff --git a/plugin-playground/src/main/kotlin/KondoKit/plugin.properties b/plugin-playground/src/main/kotlin/KondoKit/plugin.properties index 51c6d2a..629c117 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/plugin.properties +++ b/plugin-playground/src/main/kotlin/KondoKit/plugin.properties @@ -1,3 +1,3 @@ AUTHOR='downthecrop' DESCRIPTION='A plugin that adds a right-side panel with custom widgets and navigation.' -VERSION=1.1 \ No newline at end of file +VERSION=2.0 \ No newline at end of file diff --git a/plugin-playground/src/main/kotlin/KondoKit/item_configs.json b/plugin-playground/src/main/kotlin/KondoKit/res/item_configs.json similarity index 100% rename from plugin-playground/src/main/kotlin/KondoKit/item_configs.json rename to plugin-playground/src/main/kotlin/KondoKit/res/item_configs.json diff --git a/plugin-playground/src/main/kotlin/KondoKit/npc_hitpoints_map.json b/plugin-playground/src/main/kotlin/KondoKit/res/npc_hitpoints_map.json similarity index 100% rename from plugin-playground/src/main/kotlin/KondoKit/npc_hitpoints_map.json rename to plugin-playground/src/main/kotlin/KondoKit/res/npc_hitpoints_map.json diff --git a/plugin-playground/src/main/kotlin/KondoKit/res/runescape_small.ttf b/plugin-playground/src/main/kotlin/KondoKit/res/runescape_small.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4f3d764c2ff0552a1d459d3834b118897ccedf44 GIT binary patch literal 33260 zcmeHQTdd_*S^v)e%yfoMue7vwnpo zv(J>0kgzA;{?_`|cVFLH|Mmaxz30e+h+HjM7G?CoQ%8<1esbR>ksA(z`pokB()oA& z_KDA<{RNSIkDXn6;n{aR`l-Ld_@_lSt~s}|bmn6>?0gnu{|TN)&VjH{E?*Yee?R*7 zom=0&^!ogoZ=nA^k;MPzUDUcZ<8B9If&M^@@ulQaBAVw!f%dl z9=&1og}d+B*#Vs+BYAP*Bt~*)WMSvOcm8GP&v(x6yl>~Hzx`+5{=&E4_>W(bzfp-F zz5VcJ;74`w!#j7cr03l401sVr)yl`Nf5(4g?^0`jvGIxjVZ-oFS(K)rdk{MgB~+#< zI|bWMVi%i`(b=)rH?fPhuYzWgw2&k+M5w>w=z5;K?;|bRm4WRvujl zTi3~h3%~CCH_4k8zGQ7UdvM|J1=9Oo1?h(u{>9r1@?*`d-frZ&<}Kb{l!u!W-rgsN zn~UDwFSj&*Z+ZKW9A5l)Z(k$N z>pD5T?_ZtgCONtPJ=R7nH|+o26PxE>*jhb%ZhQ2R&9!G&SKhz0wYoVv zcJ%1mPCmb}a%y?${L1Lm`qJ9k$(6IuuPtpozPYh|YJ2PXJvZ8XcJ%nECr8R~Zu9);$&Kxmt&OE^=vi7D zy=VRO6X&+K&%gc1k!Rt}1@-3e^5%Lcf;*}$^Zw4!#ia|Q)6cK2ZI3RlZl5#VDtWc* zx_I&8VbuqV)HcyX$0D4$aOA?d&5K8lJ^1KDj~qL(wE_=6xiUJjxxS9E(edr=t<}@d z)9cX(Ha8xC&h?e8fF-S%INso>T@e@X4$351mVM*TW61~tuC)@Tv)kqO-{=5vVpNv zvW(I5XpPWbM|%zbPNIJn9BV51I3zcq^%P(W)Md4>n}%8XM*|BVhP^GhpgwNElLzH6 zyd1aiK*z_)UadmQ2wsoW*Cjav&5UYGo&#+Y85zmr(DJ0>i0d3i&V%x##z|i`AhYfM zFef8<5A>XtCp5p?;C{Ot!M|rcHy1j&IgAW#f_vf|jP95aS10$QJ7Mc0tTT?&$o49% zb4D+swGHd^Km2az00UwV5sQDsV$1rb?%0u!9 zS|^ah70=-((Hd!$t|KcZHdI|V6_|Cxl;9Q-!&F>KL_kVl0NKY_k|chZDAv@(lJJBDgz$KlafILR?ZHt z667i5Yq80pN)jV&iqF;c@)L7--H9W-3y&|uj?x^5x1>e31BYz%d$?4IOwSj@J%+#3 zbXl=#a7#4G5`XBVy-5t+U+;bJu3XuQ;QIifA6- zw_-d>(+KLg(>bo57Bo@|kqgz6K`%a*Eq?2PE7g8xo{)uP8XY56(vb;{{i8e51Rp~Wr#ri6TG0vyPL+ovhGul~u z1@E9l4m)^ApVs%R!AjOlW!JZ$_e{BA?P&C*6FNJ=BKk?%y!EnFBgrO*ca>Keod@1C zUQWDo@(8%!!k;=1eSH`tr@nx7%>&QW$Gt7|-88x0!%x-idR2E;-H{i@&Cz1isG}+c zI>&Szxsw9BT!H-<$dN^LswCRlFVzdDs4jBazFx5ZEeh_i+T|IR!DCj*9X_TQRqdS1 zgzFINTQ%rtL0kE%&l&Qi$CElb1tSUTM5fFSc?o>@8TMNF}n8I z&N>Td!2|V-u~V{xovs`@gFbHlrdkKFU(TF0+Ebgbp&qi2_7&`OMWu>+GtEn+4;&fi zxs03+d}DnKYsdA`%M5-%T$9L&EKp)!)oIs?S-T3QlJuiRSLciLtbkT0=UTatD#}&V zCdbMtw3b^m0(8Hk*`J8OODDgN|X+>J`c@qIyB4Bys0T>-P$JJvh+X zR8DZS(zP9)7PZbRL&Cp;E4IyJ&s&v%w7!6=7Rp#XWaa_$7^66o;bfv>FZ+ON7>o5^kWv0hhc+heJbP%Eu*wu!68>|W39v-$^Z`pS;ecnsCT?mk}y zEm`+7%fHoi^?kVZYuyyc!Lq)9cdg{CIp#aLOJHqrg$4Z@jOQjbR=?fr_m!WU3vv#t zxHC1M(WW<%&A8hLTIC~8Kw`C{58xaUQ%>8NJw%cQShEOI`P&CAl^`uhAFQ z%aq+EP9%t3ZDx*&@ zcC8JMs+SR3V@2r9dt}g^HBvoMhKIU(uj1I?{^xdRnDv5o)d8J7T&$0GqdN|J@FA&R zX@E<=?-fkm3`m{&-5;J&LZJZX25){RV>v6Bqebj~Q}!FHap;CzXkxx}c7THx&jmXB z3`UtZ?t;NJHdnLlCXJ~|>pT_E0HG1{M@rgB4Tu~e=zJv*GVBAZ-FB~!eQs>-v`Pwe zKvXTDO{{&kv-S#htK3F+8CBu*ejQFcIWu#r+i`CPhgQyH)rX2BrIXc!egUK_+_#}m zql6yynSHeD&VaUhR}7CuZrlYt;DjfRu_I*7NYZ>4yxaKj+p)Gj5oaY%C@&MhhlC`_uM%EN_ya0fM4q5gJ-VNI* z`x$h`6XVtozQ8P^tk ztpinBaxXeK#v+Cqb5`kM(mI=H_i{`v1zd*JiuSWx3A|qr_uzhKV&0h@)#jLO?E`7M zIEFT^{LWs2{uJAh&v+(x>-0Q^y*eAvs;>cNu5_-m)>&5DMLp#^xI1~#zS6*_*Wskk z8@uZh5~0a*(0~FRaYYtc1P81&v37d-8|E$cY|u0G;N2p^yY=aZz$fbK`T=6)D9Vw* zYAtz**zTKiLyzJ!mYy$Bh>Tc)QfSxuLR-$cABwBOTCZ z=Emcn2l$#8d#cIFQ7w*pHjBlvQyPpi9>yIQ5hc#H$2NSXogo6yEF`q~$_h#Kpdxg% zsCBeOpE1Et=4#AOaw!1yd?da4sT>6qP#KxlQFjjP4twGpsCSU9)sxW%C_SmKlOiY> zG03!vc5)26IFY1A5YKMUT_8y*zNk&x$#A!?{(!Dl^gZS;azbv*es&*{W2`0mQs;_S zq={M3TFA6uIMEcTTA7TxaBFP&pCN_&jig)P^_#iuV24 zB&b|tH7fe#ekrayQ*x@!54FYqQf2GTl3j=6q0V+5BL8{<$;fk3|Gf4Z#>qWw${kLP zIqVftpJN7G8?$YClVi8DBMk(Y8_o|v_b=>Cw5w5(Um=rI??wEKRBK}Dv(zdZIvm#- z3OaTJ^P-Q@14mSmVqfcOyZ2l6c8 zqw}w^V!E`3piT7y*ELq0mOTdeiBW*#Jm_m(A(rg(JMBIzL4(6&TcLMie0~p6^JUjL ze#&@nzoKGC9pbPZ)mnWBpR)NkcV0A_PTdUq$@4d)M_236@5s|AgZ#IThDq z*F3Ii%3VM66bFlxQYi2S&FF`I#T*0FT2U@gEHgfee(p-6Z+j)@an94NT1l|vpnca* znpSic_e66Gs@IQIG)8%?RBn2Ran)ka#Y-ca7oPI@dE(f-#o8TW>LG9qsm=_&4lzbM zy$CQqV=T4Ykjjh(8jET#xE^C%0rY5x1+`o#U+bvN^lqHSZIvTF@Icir2pBYtX3!ugf@eaq|h`m&e#g?(z$SL#Gsy8dDm zV+x=X*I3Y2I*{uD1*6HivW^y)NIGJi;By|WZ__wd!N^O4(?c_5KTz$^)P%jzY2VjP zjqL~V`LWdqzq32{3eerB*b3ljkrwM}Kv!oHG>yeEX%#@5`xEr=K6z%6&9c@zpSAfo z?qjL7T?IqAct+I5&u_uah&iH)3g>K)rmu|I1877EZ2;Th4SXcEfTkGZz+LD(bwR!2 z3X*+3V_v3u7B&ON3`Adjp|xxlvHznN-UF*IGx~Zz$ZzfQ%IdoxH9#G64842@AOn1^ zg&9GOE*h_nW#uGC5q|$U@yS=y)?GX~e%ap?XVpdgY`a&6z0W*!=c@Zzo8}1Pl-qMf zX0=uz4Pc}WPPcEc?V&=E%LcjBI?%jRKRs+xwZ!uVYQ9q|F+=q-r7&n`4Okai6=&uuS8 zGB|i0#LC0GO~3aBU$@4=UqFGdXC5`9eLE9cjZxccZPgBE+55@mfz{vQeaGQDcLmyU zo{S#yZ8HZtMtBDk>yz}5a7i;<+-xYtb21eB`;G`nO{8>#lJ z57lb3G^|6fz^Er;-2qhxb{O27V+OkGZdMHyW12?R88#X{4V+3@_PK9{7B?+N=)0P5I50^HF zl_!~BaYd#`PsaQHeG$SO2tT~`ILk7?AMlk`9)bm+4zmC;<{ITG?A$VcpF z_Wk-R&I#8OJ)-}Uu1euKah*-)uUCJFs}9F?rDdh#oip62KjrS^X}5l&_Lg5)yvnFY z{J#lOr$(-?HSz#{l=>F@!1A5=x#fg?e?p1OB*Bhaor(**%+8Zq%;+_@xkKx;H98Dw z4vXPhciyk^xt|O4QZF~EWtUo0o;IIuu~c2VkK281?wk#F|Kx^|@Rd51lfz>e+sTM?X$jJ=bX|C#V}_|s~X)pmejKKogSwGK9lv)>`?)uK9l>U zQk_f(J@606?^>1gA0EDJzvFwc{e0g$-zuw_E$}2EU%<1nUiX>chZwK)R~$uHjal&T z#^AoFjmNGtyH=2G7;{HN&yz#;$bpqs@tif4EqBOXXksgwR>wc6`hr!Bp*Y_Ay2$Vw zps{40LomC!&c|M!E+uFo6vMNsi(`6b1Rhq|-m1&6rcaeRKl=b7$v`#V0KGv-|r z#q8kS>}6np+G9Hb9M_WtNL{+l33wor{5`NyIg%rq;L+Uhyk)$BllS#mpMIZX`lkz! z4Bu4B`&yv|o?uwVc^47-Yh)3jem^Ct-}Bl8H|RQDTKxz$W#k2{u79n++O_ zOSv-I1oRDKG-=#z#c3UfB*21dDBBU~&XT*I%&u-NRCblE9 zSmA{yvwByObQHAt^l3FfpT08Qr|a9Nqf-5w^r>e851?wz-Ukh_jcbqS!|Sg+-2-NPFNfEwuIIWB6{FS41&`Jb>4g&Q^Hba}kP4bs1=^gU?z~HTJ>YN0 z=lxYNG--@Lic{z0iMIL!%+>2X_<_1m8iAGb4KKE)-x+w z%4RJpdun#VLZU>cx`QslwFb+L?fo~-`CIa2_9Nn0)zMe~z@qgG%XL*2w37nfaYoWe z;{R&Va~1|&TjG9JBQ4l7xZ5Vvs($C{_imq6XVX;k1a0&!{0(~K6IPRQATzYvR+H^z z)sOVb`U4Lw1AF(UU*mCCzx&RCcq1FSHVZsR26gAi-Y1>y);mxxmrk}Y*qb$s>!{)W zKL2v(5Vpco#jRI)(i}F*9aW(C%J$o0c3HJu`!!oJo_rk0vZmK-UbQjO@gs3mwHdB) zR6e+lYg1|x_KO+&mad<~8Ijfo+G;EJZ0NXM2l3nHL`$snuth#q!NV*4>HPr$x^{Sm)=K+-(3dUb?e``I$nEGGbw)?V2e{F3H^LzK~vq=B> z&OBMxY$x{^xMu%Kycdr+Ikw;BjmI$mT{%6YSH&>iHMg-^bu2lJxC3=Qf>vBpHaypS zg!SaI(f>i#)0OyN#{7-Bh;v8#NzBY^RD0T`#;#WMrq4yeSi-zz!&Nr=;a@h5bInES zT1Q2+eT}UVoD$+Z7vJ5PGd6sxPS0BJ>ZeP!@-Wt|I`Ve=y3d9@u}6kh()tZ=oeo*h z18Bv;$2-xPJg(h5zj=)IjB84CH4Ye1>lqlJ7T8VzzOsA9k))i&MkOu#6@ygvyL)K+ z$;A{ed@q7F)dEB*ZTf_sezhbdT!q1U9Fdy z-WIQw@OBaMsNII=wcWoDk(3(}em+dDEwDh#9QvyFV7BU*s&5}T@STb9@5-L9L5Y61 zFIjE|UWdJs{#E^ECsw)IpD>R6L88t)rHg7Q zPH)GJul2LLN45W_->mwCa@9&)U?r<%rD;x*(W0%`YsF6O@RKtUK7}tywPLK|u5a@` z2g%T#Oovt#+L1j_O*d#(ac(Jjwsy--;*yz3{dA zzb?;O`}{_yC;)|5Lz>dF@pX8TN+jhjChdDiidp3tCB}c_*Q=e09;%K^SuqdM4tmARE@~u)SLrR0>&0 zo~`d+E|PgN)r0UXJkT=~pQzLtnCE{su^4ML)H(!R_Xv)sPPL%R8PbUDoudV{QOJc8 zB(P=@_;PB(yX5%+fj;W}#+0YytTS@=o_A%?0O6D3Vme_H+5pv=gNk5{REXyy9-h(o)6It1+zEwR1=7g$CcIktK2V9X9iID5XDO-sRjCMB|X z-y}7UpaJVT>)Z>^|H+W+WL?B}D>;+uwPSi)R~*_pt{%~Y@YpI0eb6dBYr8m4@W#0^ zH^Xb`#C<8AMo_bAf>!k}c^^poWzbJf&+c0p)My^~HSBX!O_+CS((u>OIzQB30FFIJ z1B%WPIa*y)R+f61!AVFRK;N%1mRXIbXSLW2Ra@Tg*t{JM%y?;LAl0**`hC?7o-T$Q zD3g5goEvun5BM}gfp^VEH^(74(Nf+mW9o5+5BeP!ozK1f9S!4=qmnBUSo|E?Yn>5W|5z9Oli{9E7>|CJPkkyT%XUO%-19^i zdFyG)kOxFQ6}L5U`|Nt1R2wsEq*(m0m|%Ab^!%el2QUMeMX#N`&jl<}8!cC@8GS%6 z1Ei%6pg$aEKXf1u;6^VSwySMV1ID92=0RySg+|wBpIPS`FYo<-a+f)ovA^gL98nJj zR&{3}E&2Ea91#q0H=^)7A_-h`G{}{BfBu|Jvk;bIykh-79(p;nQ{R>UJs9MM+Ie3@ zTtDe6-gg|kavjg(xutyd6m88V_>`UpBdOmX%c+mUUTA#yNA5_E_9WaL$7wVzq(y0l=jZqvN;A)T!);o9 zE%3&n?iuh%t!BfOUaVgu@%~F0bhaIa=LGO4i_x+75iKN8Yq>v;92>p#=@PHcpwWe| zqW^!Gqc7l~_ae&dYj`m954?~8o&tb&)op+?fX@TI4#3}~Kac>BJNN|Pd-!5G_+Is0 z`~kZ)kwf7w?B4w!_D|}rjWZ4`fn^Euf7j}u~&al ziAmzyDb^LIpEa|7Vh!ardrx4@rUz9Dk!CH$Fs&~JlZuiXbY2LSKuJ|lAb zGXTin0bO_eApo}Td^6zl0Px@WpCUiD4M6*@I{;|k^&cW5=)C(N;6s4V0Kj(-^xlK^ zy`bOwKEM}6?zYh{{-kKKsy0jkHP+9FN*y1QvmQh{