diff --git a/client/src/main/java/plugin/PluginInfo.java b/client/src/main/java/plugin/PluginInfo.java index 01fddc6..1550394 100644 --- a/client/src/main/java/plugin/PluginInfo.java +++ b/client/src/main/java/plugin/PluginInfo.java @@ -13,10 +13,10 @@ import java.util.Properties; * A data class for storing information about plugins. * @author ceikry */ -class PluginInfo { - double version; - String author; - String description; +public class PluginInfo { + public double version; + public String author; + public String description; public PluginInfo(String author, String description, double version) { this.version = version; diff --git a/plugin-playground/build.gradle b/plugin-playground/build.gradle index c5f4a9f..a1644a7 100644 --- a/plugin-playground/build.gradle +++ b/plugin-playground/build.gradle @@ -110,4 +110,10 @@ task buildPlugins(type: Copy, dependsOn: classes) { from "build/classes/kotlin/main" into pluginsPath } + + // Find and copy any 'res' directories from 'src/main/kotlin/**/res/' + copy { + from fileTree(dir: "src/main/kotlin", include: "**/res/**") + into pluginsPath + } } \ No newline at end of file diff --git a/plugin-playground/src/main/kotlin/GroundItems/plugin.kt b/plugin-playground/src/main/kotlin/GroundItems/plugin.kt index ecd5ba8..8fe5b18 100644 --- a/plugin-playground/src/main/kotlin/GroundItems/plugin.kt +++ b/plugin-playground/src/main/kotlin/GroundItems/plugin.kt @@ -1,7 +1,7 @@ package GroundItems +import KondoKit.Exposed import plugin.Plugin -import plugin.annotations.PluginMeta import plugin.api.API.* import plugin.api.FontColor.fromColor import plugin.api.FontType @@ -18,27 +18,26 @@ import java.nio.charset.StandardCharsets import java.text.DecimalFormat import kotlin.math.roundToInt -@PluginMeta( - author = "downthecrop", - description = - """ - Ground Items Overlay. Just like Runelite! - cmds ::set(low,med,high,insane,hide), ::(tag,ignore)item ID, ::(reset)groundconfig - Special thanks to Chisato for the original skeleton. - """, - version = 1.2 -) -open class plugin : Plugin() { +class plugin : Plugin() { + + @Exposed(description = "Default: true, Use Local JSON or the prices from the Live/Stable server API") + private var useLiveGEPrices = true + @Exposed( "Default: 5,000 (blue)") + private var lowValue = 5000 + @Exposed( "Default: 20,000 (green)") + private var mediumValue = 20000 + @Exposed( "Default: 50,000 (orange)") + private var highValue = 50000 + @Exposed( "Default: 100,000 (pink)") + private var insaneValue = 100000 + @Exposed("Default: 0, No labels will be drawn for items below this value (unless tagged)") + private var hideBelowValue = 0 + @Exposed("Tag Items (purple) add/remove with Ctrl+RightClick Tag/Ignore") + private lateinit var taggedItems: List + @Exposed("Ignore items add/remove with Ctrl+RightClick Tag/Ignore") + private lateinit var ignoredItems: List - private var kondoExposed_lowValue = 5000 - private var kondoExposed_mediumValue = 20000 - private var kondoExposed_highValue = 50000 - private var kondoExposed_insaneValue = 100000 - private var kondoExposed_hideBelowValue = 0 - private var kondoExposed_useLiveGEPrices = true private val coindId = 995 - private lateinit var kondoExposed_taggedItems: List - private lateinit var kondoExposed_ignoredItems: List private var gePriceMap = loadGEPrices() @@ -60,23 +59,23 @@ open class plugin : Plugin() { ) override fun Init() { - kondoExposed_lowValue = GetData("low-value") as? Int ?: 5000 - kondoExposed_mediumValue = GetData("medium-value") as? Int ?: 20000 - kondoExposed_highValue = GetData("high-value") as? Int ?: 50000 - kondoExposed_insaneValue = GetData("insane-value") as? Int ?: 100000 - kondoExposed_hideBelowValue = GetData("hide-below-value") as? Int ?: 0 - kondoExposed_useLiveGEPrices = GetData("ground-item-use-remote") as? Boolean ?: true - kondoExposed_taggedItems = GetData("ground-item-tags")?.let { it.toString().split(",").mapNotNull { it.toIntOrNull() } } ?: emptyList() - kondoExposed_ignoredItems = GetData("ground-item-ignore")?.let { it.toString().split(",").mapNotNull { it.toIntOrNull() } } ?: emptyList() - if (gePriceMap.isEmpty()) SendMessage("Ground Items unable to load GE Prices, Remote: $kondoExposed_useLiveGEPrices") + lowValue = GetData("low-value") as? Int ?: 5000 + mediumValue = GetData("medium-value") as? Int ?: 20000 + highValue = GetData("high-value") as? Int ?: 50000 + insaneValue = GetData("insane-value") as? Int ?: 100000 + hideBelowValue = GetData("hide-below-value") as? Int ?: 0 + useLiveGEPrices = GetData("ground-item-use-remote") as? Boolean ?: true + taggedItems = GetData("ground-item-tags")?.let { it.toString().split(",").mapNotNull { it.toIntOrNull() } } ?: emptyList() + ignoredItems = GetData("ground-item-ignore")?.let { it.toString().split(",").mapNotNull { it.toIntOrNull() } } ?: emptyList() + if (gePriceMap.isEmpty()) SendMessage("Ground Items unable to load GE Prices, Remote: $useLiveGEPrices") } private fun isTagged(itemId: Int): Boolean { - return kondoExposed_taggedItems.contains(itemId) + return taggedItems.contains(itemId) } private fun isHidden(itemId: Int): Boolean { - return kondoExposed_ignoredItems.contains(itemId) + return ignoredItems.contains(itemId) } override fun Draw(timeDelta: Long) = renderGroundItemNames() @@ -141,10 +140,10 @@ open class plugin : Plugin() { val screenY = screenPos[1] val color = when { isTagged(itemDef.id) -> colorMap["tagged"] - highestValue < kondoExposed_lowValue -> "#FFFFFF" - highestValue < kondoExposed_mediumValue -> colorMap["lowValue"] - highestValue < kondoExposed_highValue -> colorMap["mediumValue"] - highestValue < kondoExposed_insaneValue -> colorMap["highValue"] + highestValue < lowValue -> "#FFFFFF" + highestValue < mediumValue -> colorMap["lowValue"] + highestValue < highValue -> colorMap["mediumValue"] + highestValue < insaneValue -> colorMap["highValue"] else -> colorMap["insaneValue"] } ?: "#FFFFFF" val colorInt = color.drop(1).toInt(16) @@ -183,7 +182,7 @@ open class plugin : Plugin() { val haValue = if (itemDef.id == coindId) item.value.amount else (itemDef.cost * 0.6 * item.value.amount).roundToInt() val geValue = (gePriceMap[itemDef.id.toString()]?.toInt() ?: 0) * item.value.amount val highestValue = maxOf(haValue, geValue) - return !((highestValue < kondoExposed_hideBelowValue || isHidden(itemDef.id)) && !isTagged(itemDef.id)) + return !((highestValue < hideBelowValue || isHidden(itemDef.id)) && !isTagged(itemDef.id)) } override fun OnMiniMenuCreate(currentEntries: Array?) { @@ -200,7 +199,7 @@ open class plugin : Plugin() { private fun ignoreItem(itemId: Int): Runnable { return Runnable { - val existingIgnores = kondoExposed_ignoredItems.toMutableList() + val existingIgnores = ignoredItems.toMutableList() if (existingIgnores.contains(itemId)) { existingIgnores.remove(itemId) @@ -215,7 +214,7 @@ open class plugin : Plugin() { private fun tagItem(itemId: Int): Runnable { return Runnable { - val existingTags = kondoExposed_taggedItems.toMutableList() + val existingTags = taggedItems.toMutableList() if (existingTags.contains(itemId)) { existingTags.remove(itemId) @@ -230,28 +229,28 @@ open class plugin : Plugin() { private fun resetConfig() { - kondoExposed_lowValue = 5000 - kondoExposed_mediumValue = 20000 - kondoExposed_highValue = 50000 - kondoExposed_insaneValue = 100000 - kondoExposed_hideBelowValue = 0 - kondoExposed_useLiveGEPrices = true + lowValue = 5000 + mediumValue = 20000 + highValue = 50000 + insaneValue = 100000 + hideBelowValue = 0 + useLiveGEPrices = true StoreData("ground-item-tags",""); StoreData("ground-item-ignore",""); - StoreData("low-value", kondoExposed_lowValue) - StoreData("ground-item-use-remote", kondoExposed_useLiveGEPrices) - StoreData("medium-value", kondoExposed_mediumValue) - StoreData("high-value", kondoExposed_highValue) - StoreData("insane-value", kondoExposed_insaneValue) - StoreData("hide-below-value", kondoExposed_hideBelowValue) + StoreData("low-value", lowValue) + StoreData("ground-item-use-remote", useLiveGEPrices) + StoreData("medium-value", mediumValue) + StoreData("high-value", highValue) + StoreData("insane-value", insaneValue) + StoreData("hide-below-value", hideBelowValue) } private fun displayRanges() { - val low = kondoExposed_lowValue - val medium = kondoExposed_mediumValue - val high = kondoExposed_highValue - val insane = kondoExposed_insaneValue - val hide = kondoExposed_hideBelowValue + val low = lowValue + val medium = mediumValue + val high = highValue + val insane = insaneValue + val hide = hideBelowValue SendMessage("== Ground Item Config ==") SendMessage("Low: $low") @@ -260,12 +259,12 @@ open class plugin : Plugin() { SendMessage("Insane: $insane") SendMessage("Hide Below: $hide") SendMessage("-- Ignored Items --") - for(item in kondoExposed_ignoredItems){ + for(item in ignoredItems){ val itemDef = ObjTypeList.get(item) SendMessage("Ignored: ${itemDef.name} ${itemDef.id}") } SendMessage("-- Tagged Items --") - for(item in kondoExposed_taggedItems){ + for(item in taggedItems){ val itemDef = ObjTypeList.get(item) SendMessage("Tagged: ${itemDef.name} ${itemDef.id}") } @@ -278,19 +277,19 @@ open class plugin : Plugin() { } fun OnKondoValueUpdated() { - StoreData("ground-item-tags",kondoExposed_taggedItems); - StoreData("ground-item-ignore",kondoExposed_ignoredItems); - StoreData("low-value", kondoExposed_lowValue) - StoreData("medium-value", kondoExposed_mediumValue) - StoreData("high-value", kondoExposed_highValue) - StoreData("insane-value", kondoExposed_insaneValue) - StoreData("ground-item-use-remote", kondoExposed_useLiveGEPrices) - StoreData("hide-below-value", kondoExposed_hideBelowValue) + StoreData("ground-item-tags",taggedItems); + StoreData("ground-item-ignore",ignoredItems); + StoreData("low-value", lowValue) + StoreData("medium-value", mediumValue) + StoreData("high-value", highValue) + StoreData("insane-value", insaneValue) + StoreData("ground-item-use-remote", useLiveGEPrices) + StoreData("hide-below-value", hideBelowValue) gePriceMap = loadGEPrices(); } fun loadGEPrices(): Map { - return if (kondoExposed_useLiveGEPrices) { + return if (useLiveGEPrices) { try { println("GroundItems: Loading Remote GE Prices") val url = URL("https://cdn.2009scape.org/gedata/latest.json") @@ -325,7 +324,7 @@ open class plugin : Plugin() { } else { try { println("GroundItems: Loading Local GE Prices") - BufferedReader(InputStreamReader(GroundItems.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() + "}" } diff --git a/plugin-playground/src/main/kotlin/GroundItems/plugin.properties b/plugin-playground/src/main/kotlin/GroundItems/plugin.properties index e14f44c..c77a215 100644 --- a/plugin-playground/src/main/kotlin/GroundItems/plugin.properties +++ b/plugin-playground/src/main/kotlin/GroundItems/plugin.properties @@ -5,4 +5,4 @@ Commands:\ ::(tag,ignore)item ID\ ::(reset)groundconfig\ Special thanks to Chisato for the original skeleton. -VERSION=1.2 \ No newline at end of file +VERSION=1.3 \ No newline at end of file diff --git a/plugin-playground/src/main/kotlin/GroundItems/item_configs.json b/plugin-playground/src/main/kotlin/GroundItems/res/item_configs.json similarity index 100% rename from plugin-playground/src/main/kotlin/GroundItems/item_configs.json rename to plugin-playground/src/main/kotlin/GroundItems/res/item_configs.json diff --git a/plugin-playground/src/main/kotlin/KondoKit/Helpers.kt b/plugin-playground/src/main/kotlin/KondoKit/Helpers.kt index a606805..bfcac22 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 @@ -35,9 +190,15 @@ object Helpers { } } + fun showAlert(message: String, title: String, type: Int){ + JOptionPane.showMessageDialog(null, message, title, type) + } + fun formatHtmlLabelText(text1: String, color1: Color, text2: String, color2: Color): String { - return "$text1" + - "$text2" + return "
" + + "$text1" + + "$text2" + + "
" } fun formatNumber(value: Int): String { @@ -78,13 +239,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..ecdeed0 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/HiscoresView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/HiscoresView.kt @@ -1,9 +1,21 @@ 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.POPUP_BACKGROUND +import KondoKit.plugin.Companion.POPUP_FOREGROUND +import KondoKit.plugin.Companion.TITLE_BAR_COLOR +import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR +import KondoKit.plugin.Companion.WIDGET_COLOR +import KondoKit.plugin.Companion.primaryColor +import KondoKit.plugin.Companion.secondaryColor +import KondoKit.plugin.StateManager.focusedView import com.google.gson.Gson import plugin.api.API import rt4.Sprites @@ -16,9 +28,11 @@ 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 +import kotlin.math.floor object Constants { // Sprite IDs @@ -28,35 +42,36 @@ 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_FOREGROUND_LIGHT = Color(200, 200, 200) - val COLOR_RED = Color.RED - val COLOR_SKILL_PANEL = Color(60, 60, 60) + val COLOR_BACKGROUND_DARK = WIDGET_COLOR + val COLOR_BACKGROUND_MEDIUM = VIEW_BACKGROUND_COLOR + val COLOR_FOREGROUND_LIGHT = POPUP_FOREGROUND // 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 { + + const val VIEW_NAME = "HISCORE_SEARCH_VIEW" + var hiScoreView: JPanel? = null class CustomSearchField(private val hiscoresPanel: JPanel) : Canvas() { private var cursorVisible: Boolean = true @@ -69,6 +84,7 @@ object HiscoresView { size = preferredSize minimumSize = preferredSize maximumSize = preferredSize + fillColor = COLOR_BACKGROUND_DARK } } @@ -127,7 +143,7 @@ object HiscoresView { Timer(500) { cursorVisible = !cursorVisible - if(plugin.StateManager.focusedView == "HISCORE_SEARCH_VIEW") + if(focusedView == VIEW_NAME) repaint() }.start() } @@ -140,7 +156,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() @@ -153,14 +169,16 @@ object HiscoresView { } if (text.isNotEmpty()) { - g.color = Constants.COLOR_RED + g.color = Color.RED g.drawString("x", width - 20, 20) } } 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 +186,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 +201,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 +253,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() @@ -266,13 +309,13 @@ object HiscoresView { summoning: Int, isMemberWorld: Boolean ): Double { - val base = (defence + hitpoints + Math.floor(prayer.toDouble() / 2)) * 0.25 + val base = (defence + hitpoints + floor(prayer.toDouble() / 2)) * 0.25 val melee = (attack + strength) * 0.325 - val range = Math.floor(ranged * 1.5) * 0.325 - val mage = Math.floor(magic * 1.5) * 0.325 + val range = floor(ranged * 1.5) * 0.325 + val mage = floor(magic * 1.5) * 0.325 val maxCombatType = maxOf(melee, range, mage) - val summoningFactor = if (isMemberWorld) Math.floor(summoning.toDouble() / 8) else 0.0 + val summoningFactor = if (isMemberWorld) floor(summoning.toDouble() / 8) else 0.0 return Math.round((base + maxCombatType + summoningFactor) * 1000.0) / 1000.0 } @@ -296,10 +339,10 @@ object HiscoresView { } } - fun createHiscoreSearchView(): JPanel { + fun createHiscoreSearchView() { val hiscorePanel = JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) - name = "HISCORE_SEARCH_VIEW" + name = VIEW_NAME background = Constants.COLOR_BACKGROUND_MEDIUM preferredSize = Constants.HISCORE_PANEL_DIMENSION maximumSize = preferredSize @@ -324,14 +367,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 = TOOLTIP_BACKGROUND.darker() preferredSize = Constants.FILTER_PANEL_DIMENSION maximumSize = preferredSize minimumSize = preferredSize @@ -339,7 +381,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 +390,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 +404,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 +419,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) } @@ -397,6 +440,7 @@ object HiscoresView { val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.LVL_BAR_SPRITE)); val totalLevelIcon = ImageCanvas(bufferedImageSprite).apply { + fillColor = COLOR_BACKGROUND_DARK preferredSize = Constants.ICON_DIMENSION_LARGE size = Constants.ICON_DIMENSION_LARGE } @@ -418,6 +462,7 @@ object HiscoresView { val bufferedImageSprite2 = getBufferedImageFromSprite(API.GetSprite(Constants.COMBAT_LVL_SPRITE)) val combatLevelIcon = ImageCanvas(bufferedImageSprite2).apply { + fillColor = COLOR_BACKGROUND_DARK preferredSize = Constants.ICON_DIMENSION_LARGE size = Constants.ICON_DIMENSION_LARGE } @@ -431,7 +476,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 +484,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..3a0fc14 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/LootTrackerView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/LootTrackerView.kt @@ -1,19 +1,26 @@ package KondoKit +import KondoKit.Helpers.addMouseListenerToAll import KondoKit.Helpers.formatHtmlLabelText import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.XPTrackerView.wrappedWidget +import KondoKit.plugin.Companion.IMAGE_SIZE +import KondoKit.plugin.Companion.POPUP_BACKGROUND +import KondoKit.plugin.Companion.POPUP_FOREGROUND +import KondoKit.plugin.Companion.TITLE_BAR_COLOR +import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR import KondoKit.plugin.Companion.WIDGET_COLOR import KondoKit.plugin.Companion.primaryColor import KondoKit.plugin.Companion.secondaryColor +import KondoKit.plugin.StateManager.focusedView 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,19 +29,23 @@ import java.net.URL import java.nio.charset.StandardCharsets import java.text.DecimalFormat import javax.swing.* +import kotlin.math.ceil object LootTrackerView { private const val SNAPSHOT_LIFESPAN = 10 const val BAG_ICON = 900; val npcDeathSnapshots = mutableMapOf() var gePriceMap = loadGEPrices() + const val VIEW_NAME = "LOOT_TRACKER_VIEW"; 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 +80,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,19 +105,37 @@ 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(8)) revalidate() - repaint() + if(focusedView == VIEW_NAME) + repaint() } } @@ -122,7 +151,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,12 +161,12 @@ 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(), xpPerHourLabel = l1, - progressBar = ProgressBar(0.0, Color(150, 50, 50)), + progressBar = ProgressBar(0.0, Color(0,0,0)), // unused. totalXpGained = 0, startTime = System.currentTimeMillis(), previousXp = 0 @@ -146,16 +175,19 @@ object LootTrackerView { private fun createWidgetPanel(bufferedImageSprite: BufferedImage, l1 : JLabel, l2 : JLabel): Panel { val imageCanvas = ImageCanvas(bufferedImageSprite).apply { - size = Dimension(width, height) + preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height) + minimumSize = preferredSize + maximumSize = preferredSize + size = preferredSize background = WIDGET_COLOR } - val imageContainer = Panel(FlowLayout()).apply { + val imageContainer = Panel(BorderLayout()).apply { background = WIDGET_COLOR - add(imageCanvas) + add(imageCanvas, BorderLayout.NORTH) } - return Panel(BorderLayout(5, 5)).apply { + return Panel(BorderLayout(5, 0)).apply { background = WIDGET_COLOR preferredSize = TOTAL_XP_WIDGET_SIZE add(imageContainer, BorderLayout.WEST) @@ -164,7 +196,7 @@ object LootTrackerView { } private fun createTextPanel(l1 : JLabel, l2: JLabel): Panel { - return Panel(GridLayout(2, 1, 5, 5)).apply { + return Panel(GridLayout(2, 1, 5, 0)).apply { background = WIDGET_COLOR add(l1) add(l2) @@ -173,7 +205,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,12 +221,19 @@ 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() + + if(focusedView == VIEW_NAME) + lootPanel.repaint() } } @@ -204,27 +243,93 @@ 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 { + val bufferedImageSprite = getBufferedImageFromSprite(API.GetObjSprite(itemId, quantity, true, 1, 3153952)) + + // 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) " (${formatValue(gePricePerItem)} ea)" else "" + val haText = if (quantity > 1) " (${formatValue(itemDef.cost)} ea)" else "" + val bgColor = Helpers.colorToHex(TOOLTIP_BACKGROUND) + val textColor = Helpers.colorToHex(secondaryColor) + val text = "
" + + "${itemDef.name} x $quantity
" + + "GE: ${formatValue(totalGePrice)} ${geText}
" + + "HA: ${formatValue(totalHaPrice)} ${haText}
" + + val _font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) + if (customToolTipWindow == null) { + customToolTipWindow = JWindow().apply { + contentPane = JLabel(text).apply { + border = BorderFactory.createLineBorder(Color.BLACK) + isOpaque = true + background = TOOLTIP_BACKGROUND + foreground = Color.WHITE + font = _font + } + pack() + } + } + + // Calculate the tooltip location relative to the parent component + val screenLocation = parentComponent.locationOnScreen + customToolTipWindow!!.setLocation(screenLocation.x + location.x, screenLocation.y + location.y + 20) + customToolTipWindow!!.isVisible = true + } + + // Function to hide the custom tooltip + fun hideCustomToolTip() { + customToolTipWindow?.isVisible = false + customToolTipWindow = null // Nullify the global instance + } + + private fun updateItemPanelIcon(panel: JPanel, itemId: Int, quantity: Int) { panel.removeAll() panel.add(createItemPanel(itemId, quantity).components[0], BorderLayout.CENTER) panel.revalidate() - panel.repaint() + if(focusedView == VIEW_NAME) + panel.repaint() } private fun updateKillCountLabel(lootTrackerPanel: JPanel, npcName: String) { @@ -248,7 +353,8 @@ object LootTrackerView { text = "${formatValue(newValue)} gp" putClientProperty("val", newValue) revalidate() - repaint() + if(focusedView == VIEW_NAME) + repaint() } } } @@ -308,11 +414,11 @@ 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(8)) lootTrackerView.revalidate() - lootTrackerView.repaint() + if(focusedView == VIEW_NAME) + lootTrackerView.repaint() } npcKillCounts[npcName] = npcKillCounts.getOrDefault(npcName, 0) + 1 @@ -321,7 +427,7 @@ object LootTrackerView { newDrops.forEach { drop -> val geValue = (gePriceMap[drop.id.toString()]?.toInt() ?: 0) * drop.quantity updateValueLabel(lootTrackerView, geValue.toString(), npcName) - addItemToLootPanel(lootTrackerView, drop, npcName) + plugin.registerDrawAction { addItemToLootPanel(lootTrackerView, drop, npcName) } updateTotalValue(geValue) } } @@ -330,29 +436,30 @@ 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) + background = TITLE_BAR_COLOR border = BorderFactory.createEmptyBorder(5, 5, 5, 5) - maximumSize = Dimension(270, 24) + maximumSize = Dimension(230, 24) minimumSize = maximumSize preferredSize = maximumSize } val killCount = npcKillCounts.getOrPut(npcName) { 0 } val countLabel = JLabel(formatHtmlLabelText(npcName, secondaryColor, " x $killCount", primaryColor)).apply { - foreground = Color(200, 200, 200) - font = Font("Arial", Font.PLAIN, 12) + foreground = secondaryColor + 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) + foreground = secondaryColor + font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) horizontalAlignment = JLabel.RIGHT name = "valueLabel_$npcName" } @@ -370,19 +477,120 @@ 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 = POPUP_BACKGROUND + + // Create menu items with custom font and colors + val menuItem1 = JMenuItem("Remove").apply { + font = rFont // Set custom font + background = POPUP_BACKGROUND // Dark background for item + foreground = POPUP_FOREGROUND // 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(8) 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 = POPUP_BACKGROUND + + // Create menu items with custom font and colors + val menuItem1 = JMenuItem("Reset Loot Tracker").apply { + font = rFont // Set custom font + background = POPUP_BACKGROUND // Dark background for item + foreground = POPUP_FOREGROUND // Light text color for item + } + popupMenu.add(menuItem1) + menuItem1.addActionListener { + plugin.registerDrawAction { + resetLootTracker() + } + } + return popupMenu + } + + private fun resetLootTracker(){ + lootTrackerView?.removeAll() + npcKillCounts.clear() + lootItemPanels.clear() + totalTrackerWidget = createTotalLootWidget() + + val wrapped = wrappedWidget(totalTrackerWidget!!.container) + val _popupMenu = resetLootTrackerMenu() + + // Create a custom MouseListener + val rightClickListener = object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) { + if (e.isPopupTrigger) { + _popupMenu.show(e.component, e.x, e.y) + } + } + + override fun mouseReleased(e: MouseEvent) { + if (e.isPopupTrigger) { + _popupMenu.show(e.component, e.x, e.y) + } + } + } + addMouseListenerToAll(wrapped,rightClickListener) + wrapped.addMouseListener(rightClickListener) + lootTrackerView?.add(Box.createVerticalStrut(5)) + lootTrackerView?.add(wrapped) + lootTrackerView?.add(Box.createVerticalStrut(8)) + lootTrackerView?.revalidate() + lootTrackerView?.repaint() + } 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..2c5546f 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/ProgressBar.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/ProgressBar.kt @@ -1,19 +1,22 @@ package KondoKit +import KondoKit.plugin.Companion.PROGRESS_BAR_FILL +import KondoKit.plugin.Companion.secondaryColor 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 +28,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 = PROGRESS_BAR_FILL 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, secondaryColor) // 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, secondaryColor) // 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, secondaryColor) + } + + 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 +63,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..60deccf 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt @@ -1,149 +1,324 @@ package KondoKit -import KondoKit.KondoKitUtils.convertValue +import KondoKit.Helpers.convertValue +import KondoKit.Helpers.showToast +import KondoKit.plugin.Companion.TITLE_BAR_COLOR +import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR import KondoKit.plugin.Companion.WIDGET_COLOR import KondoKit.plugin.Companion.primaryColor import KondoKit.plugin.Companion.secondaryColor +import KondoKit.plugin.StateManager.focusedView 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 + val loadedPlugins: MutableList = mutableListOf() + const val VIEW_NAME = "REFLECTIVE_EDITOR_VIEW" + 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 + loadedPlugins.clear() 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() } - reflectiveEditorView.revalidate() - reflectiveEditorView.repaint() - } - private fun addPluginToEditor(reflectiveEditorView: JPanel, plugin: Any) { - reflectiveEditorView.layout = BoxLayout(reflectiveEditorView, BoxLayout.Y_AXIS) + // Add a centered box for plugins that have no exposed fields + if (loadedPlugins.isNotEmpty()) { + val noExposedPanel = JPanel(BorderLayout()) + noExposedPanel.background = VIEW_BACKGROUND_COLOR + noExposedPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) - 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) + val label = JLabel("Loaded Plugins without Exposed Fields", SwingConstants.CENTER) + label.font = Font("RuneScape Small", Font.PLAIN, 16) label.foreground = primaryColor - label.font = Font("Arial", Font.BOLD, 14) - labelPanel.add(label, BorderLayout.CENTER) - reflectiveEditorView.add(labelPanel) - } + noExposedPanel.add(label, BorderLayout.NORTH) - for (field in exposedFields) { - field.isAccessible = true + val pluginsList = JList(loadedPlugins.toTypedArray()) + pluginsList.background = WIDGET_COLOR + pluginsList.foreground = secondaryColor + pluginsList.font = Font("RuneScape Small", Font.PLAIN, 16) - 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 gbc = GridBagConstraints() - gbc.insets = Insets(0, 5, 0, 5) // Less padding, more minimal spacing - - 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) - - 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 - ) - } + // Wrap the JList in a JScrollPane with a fixed height + val maxScrollPaneHeight = 200 + val scrollPane = JScrollPane(pluginsList).apply { + verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED + horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER } - fieldPanel.add(applyButton, gbc) - reflectiveEditorView.add(fieldPanel) + // Create a wrapper panel with BoxLayout to constrain the scroll pane + val scrollPaneWrapper = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(scrollPane) + } - 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) + noExposedPanel.add(scrollPaneWrapper, BorderLayout.CENTER) + + // Center the panel within the reflectiveEditorView + val centeredPanel = JPanel().apply { + preferredSize = Dimension(240, maxScrollPaneHeight) + maximumSize = preferredSize + minimumSize = preferredSize + } + centeredPanel.layout = BoxLayout(centeredPanel, BoxLayout.Y_AXIS) + centeredPanel.add(Box.createVerticalGlue()) + centeredPanel.add(noExposedPanel) + centeredPanel.add(Box.createVerticalGlue()) + + reflectiveEditorView.add(Box.createVerticalStrut(10)) + reflectiveEditorView.add(centeredPanel) + } + + + reflectiveEditorView.revalidate() + if(focusedView == VIEW_NAME) + reflectiveEditorView.repaint() + } + + private fun addPluginToEditor(reflectiveEditorView: JPanel, pluginInfo : PluginInfo, plugin: Plugin) { + reflectiveEditorView.layout = BoxLayout(reflectiveEditorView, BoxLayout.Y_AXIS) + + val fieldNotifier = Helpers.FieldNotifier(plugin) + val exposedFields = plugin.javaClass.declaredFields.filter { field -> + field.annotations.any { annotation -> + annotation.annotationClass.simpleName == "Exposed" + } + } + + if (exposedFields.isNotEmpty()) { + + 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 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 = TITLE_BAR_COLOR + reflectiveEditorView.add(labelPanel) + + for (field in exposedFields) { + field.isAccessible = true + + // Get the "Exposed" annotation specifically and retrieve its description, if available + val exposedAnnotation = field.annotations.firstOrNull { annotation -> + annotation.annotationClass.simpleName == "Exposed" + } + + 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 + } + } ?: "" + + 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, 8) + } + 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(5)) + } } - if (exposedFields.isNotEmpty()) { - reflectiveEditorView.add(Box.createVerticalStrut(10)) + else { + loadedPlugins.add(plugin.javaClass.`package`.name); } } -} + + var customToolTipWindow: JWindow? = null + + fun showCustomToolTip(text: String, component: JComponent) { + val _font = Font("RuneScape Small", Font.PLAIN, 16) + 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 { + val bgColor = Helpers.colorToHex(TOOLTIP_BACKGROUND) + val textColor = Helpers.colorToHex(secondaryColor) + contentPane = JLabel("
$text
").apply { + border = BorderFactory.createLineBorder(Color.BLACK) + isOpaque = true + background = TOOLTIP_BACKGROUND + 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 + val bgColor = Helpers.colorToHex(TOOLTIP_BACKGROUND) + val textColor = Helpers.colorToHex(secondaryColor) + 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..cf6444c --- /dev/null +++ b/plugin-playground/src/main/kotlin/KondoKit/ScrollablePanel.kt @@ -0,0 +1,158 @@ +package KondoKit + +import KondoKit.plugin.Companion.SCROLL_BAR_COLOR +import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR +import rt4.GameShell.frame +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 + private var lastSize = 0 + + // 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() + if(lastSize != content.preferredSize.height.coerceAtLeast(frame.height + viewBuffer)) + handleResize() + } + }, 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 + lastSize = content.preferredSize.height.coerceAtLeast(frame.height + viewBuffer) + content.bounds = Rectangle(0, 0, 242, lastSize) + 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 = SCROLL_BAR_COLOR + g2.fillRect(scrollbarX, scrollbarY, 2, scrollbarHeight) + } + } +} diff --git a/plugin-playground/src/main/kotlin/KondoKit/SpriteToBufferedImage.kt b/plugin-playground/src/main/kotlin/KondoKit/SpriteToBufferedImage.kt index e273c98..f955f95 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/SpriteToBufferedImage.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/SpriteToBufferedImage.kt @@ -4,109 +4,196 @@ import rt4.GlIndexedSprite import rt4.GlSprite import rt4.SoftwareIndexedSprite import rt4.SoftwareSprite +import java.awt.Color import java.awt.image.BufferedImage +// Define interfaces for common sprite types +interface BaseSprite { + val width: Int + val height: Int +} + +interface IndexedSprite : BaseSprite { + val pixels: ByteArray + val palette: IntArray +} + +interface NonIndexedSprite : BaseSprite { + val pixels: IntArray? +} + +// Adapter functions for existing sprite types +fun adaptSoftwareSprite(sprite: SoftwareSprite): NonIndexedSprite { + return object : NonIndexedSprite { + override val width: Int = sprite.width + override val height: Int = sprite.height + override val pixels: IntArray? = sprite.pixels + } +} + +fun adaptSoftwareIndexedSprite(sprite: SoftwareIndexedSprite): IndexedSprite { + return object : IndexedSprite { + override val width: Int = sprite.width + override val height: Int = sprite.height + override val pixels: ByteArray = sprite.pixels + override val palette: IntArray = sprite.pallet + } +} + +fun adaptGlSprite(sprite: GlSprite): NonIndexedSprite { + return object : NonIndexedSprite { + override val width: Int = sprite.width + override val height: Int = sprite.height + override val pixels: IntArray? = sprite.pixels + } +} + +fun adaptGlIndexedSprite(sprite: GlIndexedSprite): IndexedSprite { + return object : IndexedSprite { + override val width: Int = sprite.width + override val height: Int = sprite.height + override val pixels: ByteArray = sprite.pixels + override val palette: IntArray = sprite.pallet + } +} + object SpriteToBufferedImage { /** - * Converts a SoftwareSprite back into a BufferedImage. + * Converts a BaseSprite into a BufferedImage. + * + * Handles both indexed and non-indexed sprites, with optional tinting and grayscale. * * @param sprite The sprite to be converted. + * @param tint An optional Color to tint the image. + * @param grayscale If true, converts the image to grayscale. + * @param brightnessBoost A multiplier to boost the brightness of the image. * @return The BufferedImage created from the sprite. */ - fun convertToBufferedImage(sprite: SoftwareSprite): BufferedImage { + fun convertToBufferedImage( + sprite: BaseSprite, + tint: Color? = null, + grayscale: Boolean = false, + brightnessBoost: Float = 1.0f + ): BufferedImage { val width = sprite.width val height = sprite.height - val pixels = sprite.pixels - - // Create a BufferedImage with ARGB color model val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) - // Manually set pixels and print the pixel data - for (y in 0 until height) { - for (x in 0 until width) { - val pixel = pixels[y * width + x] - image.setRGB(x, y, pixel) + when (sprite) { + is IndexedSprite -> { + val pixels = sprite.pixels + val palette = sprite.palette + + // Manually set pixels using the palette + for (y in 0 until height) { + for (x in 0 until width) { + val index = pixels[y * width + x].toInt() and 0xFF + var color = palette[index] + + // Apply grayscale or tint if provided + val finalColor = if (grayscale) { + applyGrayscale(Color(color, true), brightnessBoost) + } else if (tint != null) { + applyTint(Color(color, true), tint, brightnessBoost) + } else { + applyBrightness(Color(color, true), brightnessBoost) + } + + image.setRGB(x, y, finalColor.rgb) + } + } + } + is NonIndexedSprite -> { + val pixels = sprite.pixels ?: return image // Handle null case for GlSprite + + // Manually set pixels directly + for (y in 0 until height) { + for (x in 0 until width) { + var color = pixels[y * width + x] + + // Apply grayscale or tint if provided + val finalColor = if (grayscale) { + applyGrayscale(Color(color, true), brightnessBoost) + } else if (tint != null) { + applyTint(Color(color, true), tint, brightnessBoost) + } else { + applyBrightness(Color(color, true), brightnessBoost) + } + + image.setRGB(x, y, finalColor.rgb) + } + } } } + return image } - fun convertToBufferedImage(sprite: SoftwareIndexedSprite): BufferedImage { - val width = sprite.width - val height = sprite.height - val pixels = sprite.pixels // byte[] - val palette = sprite.pallet - - // Create a BufferedImage with ARGB color model - val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) - - // Manually set pixels using the palette - for (y in 0 until height) { - for (x in 0 until width) { - // Get the index from the sprite's pixel array - val index = pixels[y * width + x].toInt() and 0xFF - // Map the index to a color in the palette - val color = palette[index] - // Set the ARGB color in the BufferedImage - image.setRGB(x, y, color) - } - } - return image + /** + * Applies a tint to a given color using the tint's alpha value to control the intensity. + * + * @param original The original color. + * @param tint The tint color to be applied. + * @param brightnessBoost A multiplier to boost the brightness of the image. + * @return The tinted color. + */ + fun applyTint(original: Color, tint: Color, brightnessBoost: Float): Color { + val boostedColor = applyBrightness(original, brightnessBoost) + val r = (boostedColor.red * tint.red / 255).coerceIn(0, 255) + val g = (boostedColor.green * tint.green / 255).coerceIn(0, 255) + val b = (boostedColor.blue * tint.blue / 255).coerceIn(0, 255) + return Color(r, g, b, boostedColor.alpha) } - fun convertToBufferedImage(sprite: GlIndexedSprite): BufferedImage { - val width = sprite.width - val height = sprite.height - val pixels = sprite.pixels // byte[] - val palette = sprite.pallet - - // Create a BufferedImage with ARGB color model - val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) - - // Manually set pixels using the palette - for (y in 0 until height) { - for (x in 0 until width) { - // Get the index from the sprite's pixel array - val index = pixels[y * width + x].toInt() and 0xFF - // Map the index to a color in the palette - val color = palette[index] - // Set the ARGB color in the BufferedImage - image.setRGB(x, y, color) - } - } - return image + /** + * Boosts the brightness of a given color. + * + * @param original The original color. + * @param factor The multiplier to boost the brightness. + * @return The color with boosted brightness. + */ + fun applyBrightness(original: Color, factor: Float): Color { + val r = (original.red * factor).coerceIn(0.0f, 255.0f).toInt() + val g = (original.green * factor).coerceIn(0.0f, 255.0f).toInt() + val b = (original.blue * factor).coerceIn(0.0f, 255.0f).toInt() + return Color(r, g, b, original.alpha) } - - fun convertToBufferedImage(sprite: GlSprite): BufferedImage { - val width = sprite.width - val height = sprite.height - val pixels = sprite.pixels - - // Create a BufferedImage with ARGB color model - val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) - - if(pixels == null) { - return image - } - - // Manually set pixels and print the pixel data - for (y in 0 until height) { - for (x in 0 until width) { - val pixel = pixels[y * width + x] - image.setRGB(x, y, pixel) - } - } - return image + /** + * Converts a color to grayscale and applies a brightness boost. + * + * @param original The original color. + * @param brightnessBoost A multiplier to boost the brightness. + * @return The grayscale version of the color with boosted brightness. + */ + fun applyGrayscale(original: Color, brightnessBoost: Float): Color { + // Calculate the grayscale value using the luminosity method + val grayValue = (0.3 * original.red + 0.59 * original.green + 0.11 * original.blue).toInt() + val boostedGray = (grayValue * brightnessBoost).coerceIn(0.0f, 255.0f).toInt() + return Color(boostedGray, boostedGray, boostedGray, original.alpha) } - fun getBufferedImageFromSprite(sprite: Any?) : BufferedImage { - return when(sprite){ - is GlSprite -> convertToBufferedImage(sprite) - is SoftwareSprite -> convertToBufferedImage(sprite) - is SoftwareIndexedSprite -> convertToBufferedImage(sprite) - is GlIndexedSprite -> convertToBufferedImage(sprite) - else -> BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) + /** + * Converts an unknown sprite object into a BufferedImage if it matches a known sprite type. + * + * @param sprite The sprite object to be converted. + * @param tint An optional Color to tint the image. + * @param grayscale If true, converts the image to grayscale. + * @param brightnessBoost A multiplier to boost the brightness of the image. + * @return The BufferedImage created from the sprite or a default image if unsupported. + */ + fun getBufferedImageFromSprite( + sprite: Any?, + tint: Color? = null, + grayscale: Boolean = false, + brightnessBoost: Float = 1.0f + ): BufferedImage { + return when (sprite) { + is SoftwareSprite -> convertToBufferedImage(adaptSoftwareSprite(sprite), tint, grayscale, brightnessBoost) + is SoftwareIndexedSprite -> convertToBufferedImage(adaptSoftwareIndexedSprite(sprite), tint, grayscale, brightnessBoost) + is GlSprite -> convertToBufferedImage(adaptGlSprite(sprite), tint, grayscale, brightnessBoost) + is GlIndexedSprite -> convertToBufferedImage(adaptGlIndexedSprite(sprite), tint, grayscale, brightnessBoost) + else -> BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) // Default empty image for unsupported types } } } \ No newline at end of file diff --git a/plugin-playground/src/main/kotlin/KondoKit/Themes.kt b/plugin-playground/src/main/kotlin/KondoKit/Themes.kt new file mode 100644 index 0000000..1b7988d --- /dev/null +++ b/plugin-playground/src/main/kotlin/KondoKit/Themes.kt @@ -0,0 +1,94 @@ +package KondoKit + +import java.awt.Color + +object Themes { + enum class ThemeType { + RUNELITE, + DARKER_RUNELITE, + SARADOMIN, + ORION, + } + + fun getTheme(themeType: ThemeType): Theme { + return when (themeType) { + ThemeType.RUNELITE -> Theme( + widgetColor = Color(30, 30, 30), + titleBarColor = Color(21, 21, 21), + viewBackgroundColor = Color(40, 40, 40), + primaryColor = Color(165, 165, 165), + secondaryColor = Color(255, 255, 255), + popupBackground = Color(45, 45, 45), + popupForeground = Color(220, 220, 220), + tooltipBackground = Color(50, 50, 50), + scrollBarColor = Color(64, 64, 64), + progressBarFill = Color(61, 56, 49), + navTint = null, + navGreyScale = false, + boost = 1f + ) + ThemeType.DARKER_RUNELITE -> Theme( + widgetColor = Color(15, 15, 15), + titleBarColor = Color(10, 10, 10), + viewBackgroundColor = Color(20, 20, 20), + primaryColor = Color(140, 140, 140), + secondaryColor = Color(210, 210, 210), + popupBackground = Color(25, 25, 25), + popupForeground = Color(200, 200, 200), + tooltipBackground = Color(30, 30, 30), + scrollBarColor = Color(40, 40, 40), + progressBarFill = Color(45, 40, 35), + navTint = null, + navGreyScale = false, + boost = 0.8f + ) + ThemeType.ORION -> Theme( + widgetColor = Color(50, 50, 50), + titleBarColor = Color(35, 35, 35), + viewBackgroundColor = Color(60, 60, 60), + primaryColor = Color(180, 180, 180), + secondaryColor = Color(210, 210, 210), + popupBackground = Color(45, 45, 45), + popupForeground = Color(230, 230, 230), + tooltipBackground = Color(55, 55, 55), + scrollBarColor = Color(75, 75, 75), + progressBarFill = Color(100, 100, 100), + navTint = null, + navGreyScale = true, + boost = 1.3f + ) + ThemeType.SARADOMIN -> Theme( + widgetColor = Color(62, 53, 41), + titleBarColor = Color(111, 93, 69).darker(), + viewBackgroundColor = Color(101, 85, 63), + primaryColor = Color(180, 150, 120), + secondaryColor = Color(230, 210, 190), + popupBackground = Color(70, 56, 42), + popupForeground = Color(220, 200, 180), + tooltipBackground = Color(80, 65, 50), + scrollBarColor = Color(100, 85, 70), + progressBarFill = Color(130, 110, 90), + navTint = null, + navGreyScale = false, + boost = 1f + ) + } + } + + + data class Theme( + val widgetColor: Color, + val titleBarColor: Color, + val viewBackgroundColor: Color, + val primaryColor: Color, + val secondaryColor: Color, + val popupBackground: Color, + val popupForeground: Color, + val tooltipBackground: Color, + val scrollBarColor: Color, + val progressBarFill: Color, + val navTint: Color?, + val navGreyScale: Boolean, + val boost: Float + ) +} diff --git a/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt b/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt index 0472ce9..b975194 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt @@ -1,35 +1,43 @@ package KondoKit +import KondoKit.Helpers.addMouseListenerToAll import KondoKit.Helpers.formatHtmlLabelText import KondoKit.Helpers.formatNumber import KondoKit.Helpers.getProgressBarColor import KondoKit.Helpers.getSpriteId import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.plugin.Companion.IMAGE_SIZE +import KondoKit.plugin.Companion.LVL_ICON +import KondoKit.plugin.Companion.POPUP_BACKGROUND +import KondoKit.plugin.Companion.POPUP_FOREGROUND 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 KondoKit.plugin.StateManager.focusedView 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 + const val VIEW_NAME = "XP_TRACKER_VIEW" + 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 +82,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) @@ -92,24 +100,61 @@ object XPTrackerView { xpWidget.xpGainedLabel.text = formatHtmlLabelText("XP Gained: ", primaryColor, formattedXp, secondaryColor) // Update the progress bar with current level, progress, and next level - xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, plugin.StateManager.focusedView == "XP_TRACKER_VIEW") + xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, focusedView == VIEW_NAME) xpWidget.previousXp = xp - if (plugin.StateManager.focusedView == "XP_TRACKER_VIEW") - xpWidget.panel.repaint() + if (focusedView == VIEW_NAME) + 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() + if (focusedView == VIEW_NAME) + 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 (focusedView == VIEW_NAME) + xpTrackerView.repaint() + } fun createTotalXPWidget(): XPWidget { val widgetPanel = Panel().apply { @@ -120,43 +165,39 @@ 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 - size = IMAGE_SIZE + preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height) + maximumSize = preferredSize + minimumSize = preferredSize + size = preferredSize } 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") + if(focusedView == VIEW_NAME) imageContainer.repaint() } 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,14 +218,14 @@ 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 this.font = font }, xpPerHourLabel = xpPerHourLabel, - progressBar = ProgressBar(0.0, Color(150, 50, 50)), + progressBar = ProgressBar(0.0, Color.BLACK), // Unused totalXpGained = 0, startTime = System.currentTimeMillis(), previousXp = 0, @@ -193,19 +234,66 @@ 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 = POPUP_BACKGROUND + + // Create menu items with custom font and colors + val menuItem1 = JMenuItem("Reset Tracker").apply { + font = rFont // Set custom font + background = POPUP_BACKGROUND // Dark background for item + foreground = POPUP_FOREGROUND // Light text color for item + } + + // Add menu items to the popup menu + popupMenu.add(menuItem1) + + // Add action listeners to each menu item (optional) + menuItem1.addActionListener { plugin.registerDrawAction { resetXPTracker(xpTrackerView!!) } } + return popupMenu + } + + fun createXPWidget(skillId: Int, previousXp: Int): XPWidget { val widgetPanel = Panel().apply { layout = BorderLayout(5, 5) @@ -237,19 +325,19 @@ object XPTrackerView { imageContainer.size = Dimension(image.width, image.height) // Ensure container respects the image size imageContainer.revalidate() - if(plugin.StateManager.focusedView == "XP_TRACKER_VIEW") + if(focusedView == VIEW_NAME) imageContainer.repaint() } 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 @@ -278,7 +366,7 @@ object XPTrackerView { val levelPanel = Panel().apply { layout = BorderLayout(5, 0) - background = Color(43, 43, 43) + background = WIDGET_COLOR } val progressBarPanel = ProgressBar(0.0, getProgressBarColor(skillId)).apply { @@ -297,12 +385,12 @@ object XPTrackerView { widgetPanel.add(levelPanel, BorderLayout.SOUTH) widgetPanel.revalidate() - if(plugin.StateManager.focusedView == "XP_TRACKER_VIEW") + if(focusedView == VIEW_NAME) widgetPanel.repaint() return XPWidget( skillId = skillId, - panel = widgetPanel, + container = widgetPanel, xpGainedLabel = xpGainedLabel, xpLeftLabel = xpLeftLabel, xpPerHourLabel = xpPerHourLabel, @@ -314,18 +402,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 +431,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..1fde673 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/plugin.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/plugin.kt @@ -4,23 +4,32 @@ import KondoKit.Constants.COMBAT_LVL_SPRITE import KondoKit.Helpers.formatHtmlLabelText import KondoKit.Helpers.formatNumber import KondoKit.Helpers.getSpriteId +import KondoKit.Helpers.showAlert 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.Themes.Theme +import KondoKit.Themes.ThemeType +import KondoKit.Themes.getTheme 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.* @@ -39,36 +48,76 @@ 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 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_SIZE = Dimension(220, 50) + val TOTAL_XP_WIDGET_SIZE = Dimension(220, 30) + val IMAGE_SIZE = Dimension(25, 23) + + // Default Theme Colors + var WIDGET_COLOR = Color(30, 30, 30) + var TITLE_BAR_COLOR = Color(21, 21, 21) + var VIEW_BACKGROUND_COLOR = Color(40, 40, 40) + var primaryColor = Color(165, 165, 165) // Color for "XP Gained:" + var secondaryColor = Color(255, 255, 255) // Color for "0" + var POPUP_BACKGROUND = Color(45, 45, 45) + var POPUP_FOREGROUND = Color(220, 220, 220) + var TOOLTIP_BACKGROUND = Color(50,50,50) + var SCROLL_BAR_COLOR = Color(64, 64, 64) + var PROGRESS_BAR_FILL = Color(61, 56, 49) + var NAV_TINT: Color? = null + var NAV_GREYSCALE = false + var BOOST = 1f + + var appliedTheme = ThemeType.RUNELITE + + @Exposed("Theme colors for KondoKit, requires a relaunch to apply.") + var theme = ThemeType.RUNELITE + + @Exposed("Default: true, Use Local JSON or the prices from the Live/Stable server API") + var useLiveGEPrices = true + + @Exposed("Used to calculate Combat Actions until next level.") + var playerXPMultiplier = 5 + + @Exposed("Start minimized/collapsed by default") + var launchMinimized = false + + @Exposed("Default 16 on Windows, 0 Linux/macOS. If Kondo is not " + + "perfectly snapped to the edge of the game due to window chrome you can update this to fix it") + var uiOffset = 0 + + private const val FIXED_WIDTH = 765 + 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 initialized = false + private var lastClickTime = 0L + private var lastUIOffset = 0 + private const val HIDDEN_VIEW = "HIDDEN" + private val drawActions = mutableListOf<() -> Unit>() + + fun registerDrawAction(action: () -> Unit) { + synchronized(drawActions) { + drawActions.add(action) + } + } } fun allSpritesLoaded() : Boolean { @@ -76,62 +125,77 @@ class plugin : Plugin() { try{ for (i in 0 until 24) { if(!js5Archive8.isFileReady(getSpriteId(i))){ - return false; + return false } } - val otherIcons = arrayOf(LVL_ICON, MAG_SPRITE, LOOT_ICON, WRENCH_ICON, COMBAT_LVL_SPRITE, BAG_ICON); + val otherIcons = arrayOf(LVL_ICON, MAG_SPRITE, LOOT_ICON, WRENCH_ICON, COMBAT_LVL_SPRITE, BAG_ICON) for (icon in otherIcons) { if(!js5Archive8.isFileReady(icon)){ - return false; + return false } } } catch (e : Exception){ - return false; + return false } - return true; + return true } override fun OnLogin() { 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() } + override fun Init() { + // Disable Font AA + System.setProperty("sun.java2d.opengl", "false") + System.setProperty("awt.useSystemAAFontSettings", "off") + System.setProperty("swing.aatext", "false") + } + private fun UpdateDisplaySettings() { val mode = GetWindowMode() + val currentScrollPaneWidth = if (mainContentPanel.isVisible) NAVBAR_WIDTH + MAIN_CONTENT_WIDTH else NAVBAR_WIDTH + lastUIOffset = uiOffset 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 + uiOffset) { + frame.setSize(FIXED_WIDTH + currentScrollPaneWidth + uiOffset, frame.height) } - val difference = frame.width - (FIXED_WIDTH + SCROLLPANE_WIDTH) + + val difference = frame.width - (FIXED_WIDTH + uiOffset + currentScrollPaneWidth) GameShell.leftMargin = difference / 2 } WindowMode.RESIZABLE -> { - GameShell.canvasWidth -= SCROLLPANE_WIDTH + GameShell.canvasWidth = frame.width - (currentScrollPaneWidth + uiOffset) } } - 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("kondoTheme", theme.toString()) + if(appliedTheme != theme) { + showAlert( + "KondoKit Theme changes require a relaunch.", + "KondoKit", + JOptionPane.INFORMATION_MESSAGE + ) + } + StoreData("kondoPlayerXPMultiplier", playerXPMultiplier) LootTrackerView.gePriceMap = LootTrackerView.loadGEPrices() + StoreData("kondoLaunchMinimized", launchMinimized) + StoreData("kondoUIOffset", uiOffset) + if(lastUIOffset != uiOffset){ + UpdateDisplaySettings() + reloadInterfaces = true + } } override fun OnMiniMenuCreate(currentEntries: Array?) { @@ -139,12 +203,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 +220,31 @@ class plugin : Plugin() { private fun searchHiscore(username: String): Runnable { return Runnable { - cardLayout.show(mainContentPanel, "HISCORE_SEARCH_VIEW") - StateManager.focusedView = "HISCORE_SEARCH_VIEW" + setActiveView(HiscoresView.VIEW_NAME) 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 +262,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 == XPTrackerView.VIEW_NAME) xpTrackerView?.repaint() updateWidget(xpWidget, xp) @@ -221,23 +280,36 @@ 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 } + // Draw synced actions (that require to be done between glBegin and glEnd) + if (drawActions.isNotEmpty()) { + synchronized(drawActions) { + val actionsCopy = drawActions.toList() + drawActions.clear() + for (action in actionsCopy) { + action() + } + } + } // 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,39 +317,115 @@ 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") + loadFont() + val themeIndex = (GetData("kondoTheme") as? String) ?: "RUNELITE" + theme = ThemeType.valueOf(themeIndex) + applyTheme(getTheme(theme)) + appliedTheme = theme + + try { + UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel") + + // Modify the UI properties to match theme + UIManager.put("control", VIEW_BACKGROUND_COLOR) + UIManager.put("info", VIEW_BACKGROUND_COLOR) + UIManager.put("nimbusBase", WIDGET_COLOR) + UIManager.put("nimbusBlueGrey", TITLE_BAR_COLOR) + + UIManager.put("nimbusDisabledText", primaryColor) + UIManager.put("nimbusSelectedText", secondaryColor) + UIManager.put("text", secondaryColor) + + UIManager.put("nimbusFocus", TITLE_BAR_COLOR) + UIManager.put("nimbusInfoBlue", POPUP_BACKGROUND) + UIManager.put("nimbusLightBackground", WIDGET_COLOR) + UIManager.put("nimbusSelectionBackground", PROGRESS_BAR_FILL) + + UIManager.put("Button.background", WIDGET_COLOR) + UIManager.put("Button.foreground", secondaryColor) + + UIManager.put("CheckBox.background", VIEW_BACKGROUND_COLOR) + UIManager.put("CheckBox.foreground", secondaryColor) + UIManager.put("CheckBox.icon", UIManager.getIcon("CheckBox.icon")) + + UIManager.put("ComboBox.background", WIDGET_COLOR) + UIManager.put("ComboBox.foreground", secondaryColor) + UIManager.put("ComboBox.selectionBackground", PROGRESS_BAR_FILL) + UIManager.put("ComboBox.selectionForeground", primaryColor) + UIManager.put("ComboBox.buttonBackground", WIDGET_COLOR) + + UIManager.put("Spinner.background", WIDGET_COLOR) + UIManager.put("Spinner.foreground", secondaryColor) + UIManager.put("Spinner.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR)) + + UIManager.put("TextField.background", WIDGET_COLOR) + UIManager.put("TextField.foreground", secondaryColor) + UIManager.put("TextField.caretForeground", secondaryColor) + UIManager.put("TextField.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR)) + + UIManager.put("ScrollBar.thumb", WIDGET_COLOR) + UIManager.put("ScrollBar.track", VIEW_BACKGROUND_COLOR) + UIManager.put("ScrollBar.thumbHighlight", TITLE_BAR_COLOR) + + UIManager.put("ProgressBar.foreground", PROGRESS_BAR_FILL) + UIManager.put("ProgressBar.background", WIDGET_COLOR) + UIManager.put("ProgressBar.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR)) + + UIManager.put("ToolTip.background", VIEW_BACKGROUND_COLOR) + UIManager.put("ToolTip.foreground", secondaryColor) + UIManager.put("ToolTip.border", BorderFactory.createLineBorder(TITLE_BAR_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 + val osName = System.getProperty("os.name").toLowerCase() + uiOffset = (GetData("kondoUIOffset") as? Int) ?: if (osName.contains("win")) 16 else 0 + 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!!), XPTrackerView.VIEW_NAME) + mainContentPanel.add(ScrollablePanel(hiScoreView!!), HiscoresView.VIEW_NAME) + mainContentPanel.add(ScrollablePanel(lootTrackerView!!), LootTrackerView.VIEW_NAME) + mainContentPanel.add(ScrollablePanel(reflectiveEditorView!!), ReflectiveEditorView.VIEW_NAME) 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")) - navPanel.add(createNavButton(MAG_SPRITE, "HISCORE_SEARCH_VIEW")) - navPanel.add(createNavButton(LOOT_ICON, "LOOT_TRACKER_VIEW")) - navPanel.add(createNavButton(WRENCH_ICON, "REFLECTIVE_EDITOR_VIEW")) + navPanel.add(createNavButton(LVL_ICON, XPTrackerView.VIEW_NAME)) + navPanel.add(createNavButton(MAG_SPRITE, HiscoresView.VIEW_NAME)) + navPanel.add(createNavButton(LOOT_ICON, LootTrackerView.VIEW_NAME)) + navPanel.add(createNavButton(WRENCH_ICON, ReflectiveEditorView.VIEW_NAME)) - 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 +433,189 @@ 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(XPTrackerView.VIEW_NAME) + } else { + setActiveView(HIDDEN_VIEW) + } 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_VIEW) { + 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), NAV_TINT, NAV_GREYSCALE, BOOST) + 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 = "" } + + fun applyTheme(theme: Theme) { + WIDGET_COLOR = theme.widgetColor + TITLE_BAR_COLOR = theme.titleBarColor + VIEW_BACKGROUND_COLOR = theme.viewBackgroundColor + primaryColor = theme.primaryColor + secondaryColor = theme.secondaryColor + POPUP_BACKGROUND = theme.popupBackground + POPUP_FOREGROUND = theme.popupForeground + TOOLTIP_BACKGROUND = theme.tooltipBackground + SCROLL_BAR_COLOR = theme.scrollBarColor + PROGRESS_BAR_FILL = theme.progressBarFill + NAV_TINT = theme.navTint + NAV_GREYSCALE = theme.navGreyScale + BOOST = theme.boost + } } 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 0000000..4f3d764 Binary files /dev/null and b/plugin-playground/src/main/kotlin/KondoKit/res/runescape_small.ttf differ diff --git a/plugin-playground/src/main/kotlin/LoginTimer/plugin.kt b/plugin-playground/src/main/kotlin/LoginTimer/plugin.kt index 0ba67f7..1b8f60b 100644 --- a/plugin-playground/src/main/kotlin/LoginTimer/plugin.kt +++ b/plugin-playground/src/main/kotlin/LoginTimer/plugin.kt @@ -25,6 +25,7 @@ class plugin : Plugin() { private var timeMode = TIME_MODE_INITIALIZATION private var initTime: Long = 0 + private var logoutFlag = true private var displayMessageCounter = 0 private var component: Component? = null @@ -35,6 +36,20 @@ class plugin : Plugin() { displayMessageCounter = 0 } + override fun OnPluginsReloaded(): Boolean { + return true + } + + override fun OnLogin() { + if(logoutFlag) + initTime = System.currentTimeMillis() + logoutFlag = false + } + + override fun OnLogout() { + logoutFlag = true + } + override fun Draw(timeDelta: Long) { if (component == null) return @@ -134,6 +149,9 @@ class plugin : Plugin() { API.InsertMiniMenuEntry("Disable Timer", "") { timeMode = DEFAULT_TIME_MODE } + API.InsertMiniMenuEntry("Reset Play Time", "") { + initTime = System.currentTimeMillis() + } } } } \ No newline at end of file diff --git a/plugin-playground/src/main/kotlin/LoginTimer/plugin.properties b/plugin-playground/src/main/kotlin/LoginTimer/plugin.properties index 1ebf1f5..1f9beaa 100644 --- a/plugin-playground/src/main/kotlin/LoginTimer/plugin.properties +++ b/plugin-playground/src/main/kotlin/LoginTimer/plugin.properties @@ -1,3 +1,3 @@ AUTHOR='Woahscam, Ceikry' DESCRIPTION='Displays the session time played, system time, or no time over the "Report Abuse" button.' -VERSION=1.2 \ No newline at end of file +VERSION=1.1 \ No newline at end of file diff --git a/plugin-playground/src/main/kotlin/ToggleResizableSD/plugin.kt b/plugin-playground/src/main/kotlin/ToggleResizableSD/plugin.kt index 1eb5198..58ed363 100644 --- a/plugin-playground/src/main/kotlin/ToggleResizableSD/plugin.kt +++ b/plugin-playground/src/main/kotlin/ToggleResizableSD/plugin.kt @@ -1,81 +1,89 @@ package ToggleResizableSD +import KondoKit.Exposed import plugin.Plugin -import plugin.annotations.PluginMeta import plugin.api.API -import rt4.* +import plugin.api.API.StoreData +import rt4.DisplayMode +import rt4.GameShell +import rt4.InterfaceList +import rt4.client import java.awt.event.KeyAdapter import java.awt.event.KeyEvent class plugin : Plugin() { - var toggleResize = false - var wantHd = false //Setting wantHd to true hides the black screen on logout (when resize SD is enabled), by enabling HD on logout + + @Exposed("Use Resizable SD") + var useResizable = false + + @Exposed("Setting wantHd to true hides the black screen on logout (when resize SD is enabled), by enabling HD on logout ") + var wantHd = false + override fun Init() { API.AddKeyboardListener(object : KeyAdapter() { override fun keyPressed(e: KeyEvent) { if (e.keyCode == KeyEvent.VK_F12) { - toggleResize = true + toggleResizableSd() } } }) - if (!DisplayMode.resizableSD && API.GetData("use-resizable-sd") == true) { - toggleResize = true + + useResizable = DisplayMode.resizableSD + if (API.GetData("use-resizable-sd") == true) { + useResizable = true } - var osNameLowerCase: String = "" - var osName: String - - try { - osName = System.getProperty("os.name") - } catch (e: Exception) { - osName = "Unknown" - } - - osNameLowerCase = osName.toLowerCase() + + var osNameLowerCase: String = System.getProperty("os.name").toLowerCase() if (!osNameLowerCase.startsWith("mac")) { wantHd = true } + if (API.GetData("want-hd") == false) { wantHd = false } } - + override fun ProcessCommand(commandStr: String, args: Array?) { - when(commandStr.toLowerCase()) { + when (commandStr.toLowerCase()) { "::toggleresizablesd", "::resizablesd", "::togglersd", "::rsd" -> { - toggleResize = true //We could call toggleResizableSd() directly here, but it's not necessary. + toggleResizableSd() } - "::toggleresizablesdhd", "::resizablesdhd", "::togglersdhd", "::rsdhd", -> { + "::toggleresizablesdhd", "::resizablesdhd", "::togglersdhd", "::rsdhd" -> { wantHd = !wantHd - API.StoreData("want-hd", wantHd) + StoreData("want-hd", wantHd) API.SendMessage("You have turned login screen HD " + (if (wantHd) "on" else "off")) } } } - + fun toggleResizableSd() { //We only want to toggle resizable SD when we are logged in and the lobby/welcome interface is not open. if (InterfaceList.aClass13_26 == null || client.gameState != 30) { return } - toggleResize = false - DisplayMode.resizableSD = !DisplayMode.resizableSD; - if(!DisplayMode.resizableSD){ - //Revert to fixed - API.StoreData("use-resizable-sd", false) //Note: It is important to call StoreData before setWindowMode because setWindowMode causes all plugins to reload. + + DisplayMode.resizableSD = !DisplayMode.resizableSD + useResizable = DisplayMode.resizableSD + StoreData("use-resizable-sd", useResizable) + + if (!DisplayMode.resizableSD) { DisplayMode.setWindowMode(true, 0, -1, -1) } else { - //Use resizable - API.StoreData("use-resizable-sd", true) //Note: It is important to call StoreData before setWindowMode because setWindowMode causes all plugins to reload. DisplayMode.setWindowMode(true, 0, GameShell.frameWidth, GameShell.frameHeight) } } - + override fun Draw(timeDelta: Long) { - if (toggleResize) { + if (useResizable != DisplayMode.resizableSD) { toggleResizableSd() } } - + + fun OnKondoValueUpdated() { + StoreData("want-hd", wantHd) + StoreData("use-resizable-sd", useResizable) + } + override fun OnLogout() { if (DisplayMode.resizableSD && wantHd) { //Because resizable SD always uses the "HD" size canvas/window mode (check the in-game Graphics Options with resizeable SD enabled if you don't believe me!), useHD becomes true when logging out, so logging out with resizeSD enabled means "HD" will always be enabled on the login screen after logging out, so we might as well fix the HD flyover by setting resizableSD to false first, and then calling setWindowMode to replace the canvas and set newMode to 2. diff --git a/plugin-playground/src/main/kotlin/ToggleResizableSD/plugin.properties b/plugin-playground/src/main/kotlin/ToggleResizableSD/plugin.properties index 08a1c2b..54fa4cc 100644 --- a/plugin-playground/src/main/kotlin/ToggleResizableSD/plugin.properties +++ b/plugin-playground/src/main/kotlin/ToggleResizableSD/plugin.properties @@ -1,3 +1,3 @@ AUTHOR='ipkpjersi' DESCRIPTION='Allows you to use F12 to toggle resizable SD.' -VERSION=1.0 \ No newline at end of file +VERSION=1.1 \ No newline at end of file diff --git a/plugin-playground/src/main/kotlin/XPDropPlugin/plugin.kt b/plugin-playground/src/main/kotlin/XPDropPlugin/plugin.kt index 24f1feb..e857ef1 100644 --- a/plugin-playground/src/main/kotlin/XPDropPlugin/plugin.kt +++ b/plugin-playground/src/main/kotlin/XPDropPlugin/plugin.kt @@ -1,12 +1,34 @@ -package XPDropPlugin; - +package XPDropPlugin + +import KondoKit.Exposed import plugin.Plugin -import plugin.annotations.PluginMeta -import plugin.api.* +import plugin.api.API +import plugin.api.API.* +import plugin.api.FontColor.fromColor +import plugin.api.FontType +import plugin.api.TextModifier +import plugin.api.WindowMode +import rt4.Sprite +import rt4.client import java.awt.Color +import java.awt.image.BufferedImage +import java.io.InputStream +import javax.imageio.ImageIO import kotlin.math.ceil + class plugin : Plugin() { + + enum class Theme { + DEFAULT, RUNELITE + } + + @Exposed + private var theme = Theme.DEFAULT + + @Exposed + private var alwaysShow = false + private val displayTimeout = 10000L // 10 seconds private val drawStart = 175 private val drawPadding = 25 @@ -15,41 +37,50 @@ class plugin : Plugin() { private var totalXp = 0 private val activeGains = ArrayList() private var lastGain = 0L + private val IN_GAME = 30 + + private val spriteCache = HashMap() + + override fun Init() { + val themeIndex = (GetData("xp-drop-theme") as? String) ?: "DEFAULT" + theme = Theme.valueOf(themeIndex) + alwaysShow = (GetData("xp-drop-alwaysShow") as? Boolean) ?: false + } override fun Draw(deltaTime: Long) { - if (System.currentTimeMillis() - lastGain >= displayTimeout && activeGains.isEmpty()) - return + if (shouldSkipDrawing()) return drawTotalXPBox() - val removeList = ArrayList() - var posX = API.GetWindowDimensions().width / 2 - if (API.GetWindowMode() == WindowMode.FIXED) - posX += 60 + val movementSpeedFactor = deltaTime / 16.666 // 60 FPS - for(gain in activeGains) { - gain.currentPos -= ceil(deltaTime / 20.0).toInt() + for (gain in activeGains) { + gain.currentPos -= ceil(movementSpeedFactor).toInt() // Adjust movement based on deltaTime if (gain.currentPos <= drawClear) { removeList.add(gain) totalXp += gain.xp - } else if (gain.currentPos <= drawStart){ - val sprite = XPSprites.getSpriteForSkill(skillId = gain.skill) - sprite?.render(posX - 25, gain.currentPos - 20) - API.DrawText( - FontType.SMALL, - FontColor.fromColor(Color.WHITE), - TextModifier.LEFT, - addCommas(gain.xp.toString()), - posX, - gain.currentPos - ) + } else if (gain.currentPos <= drawStart) { + drawXPDrops(gain) } } activeGains.removeAll(removeList.toSet()) } + private fun shouldSkipDrawing(): Boolean { + return client.gameState < IN_GAME || (!alwaysShow && isDisplayTimeoutExpired() && activeGains.isEmpty()) + } + + fun OnKondoValueUpdated() { + StoreData("xp-drop-theme",theme.toString()) + StoreData("xp-drop-alwaysShow",alwaysShow) + } + + private fun isDisplayTimeoutExpired(): Boolean { + return System.currentTimeMillis() - lastGain >= displayTimeout + } + override fun OnXPUpdate(skill: Int, xp: Int) { if (xp == lastXp[skill]) return @@ -81,6 +112,54 @@ class plugin : Plugin() { } private fun drawTotalXPBox() { + when (theme) { + Theme.DEFAULT -> drawDefaultXPBox() + Theme.RUNELITE -> drawRuneliteXPBox() + } + } + + private fun drawXPDrops(gain : XPGain) { + when (theme) { + Theme.DEFAULT -> drawDefaultXPDrop(gain) + Theme.RUNELITE -> drawRuneliteXPDrops(gain) + } + } + + private fun drawDefaultXPDrop(gain: XPGain) { + var posX = API.GetWindowDimensions().width / 2 + if (API.GetWindowMode() == WindowMode.FIXED) + posX += 60 + val sprite = spriteCache.getOrPut(gain.skill) { XPSprites.getSpriteForSkill(skillId = gain.skill) } + sprite?.render(posX - 25, gain.currentPos - 20) + DrawText( + FontType.SMALL, + fromColor(Color.WHITE), + TextModifier.LEFT, + addCommas(gain.xp.toString()), + posX, + gain.currentPos + ) + } + + private fun drawRuneliteXPDrops(gain: XPGain) { + val w = API.GetWindowDimensions().width + val offset = if(API.GetWindowMode() == WindowMode.FIXED) 251 else 225 + val extra = 2; + val posX = w - (offset + extra) + + val str = addCommas(gain.xp.toString()) + val fontCharWidth = 4 + val displace = str.length*fontCharWidth + 30 + + // should be scaled https://github.com/runelite/runelite/blob/0500906f8de9cd20875c168a7a59e5e066ed5058/runelite-client/src/main/java/net/runelite/client/game/SkillIconManager.java#L50 + // but for now this is good enough + + val sprite = spriteCache.getOrPut(gain.skill) { XPSprites.getSpriteForSkill(skillId = gain.skill) } + sprite?.render(posX - displace, gain.currentPos - 20) + drawTextWithDropShadow(posX, gain.currentPos, Color.WHITE, addCommas(gain.xp.toString())) + } + + private fun drawDefaultXPBox() { var posX = API.GetWindowDimensions().width / 2 val posY = API.GetWindowDimensions().height / 4 @@ -89,13 +168,13 @@ class plugin : Plugin() { API.ClipRect(0, 0, posX * 2, posY * 4) - val horizontal = API.GetSprite(822) - val horizontalTop = API.GetSprite(820) - val tlCorner = API.GetSprite(824) - val blCorner = API.GetSprite(826) - val trCorner = API.GetSprite(825) - val brCorner = API.GetSprite(827) - val bg = API.GetSprite(657) + val horizontal = spriteCache.getOrPut(822) { API.GetSprite(822) } + val horizontalTop = spriteCache.getOrPut(820) { API.GetSprite(820) } + val tlCorner = spriteCache.getOrPut(824) { API.GetSprite(824) } + val blCorner = spriteCache.getOrPut(826) { API.GetSprite(826) } + val trCorner = spriteCache.getOrPut(825) { API.GetSprite(825) } + val brCorner = spriteCache.getOrPut(827) { API.GetSprite(827) } + val bg = spriteCache.getOrPut(657) { API.GetSprite(657) } bg?.render(posX - 77, 10) API.FillRect(posX - 75, 5, 140, 30, 0, 64) @@ -112,26 +191,58 @@ class plugin : Plugin() { horizontalTop?.render(posX + 9, -8) horizontal?.render(posX + 9, 22) - API.DrawText( - FontType.SMALL, - FontColor.fromColor(Color.WHITE), - TextModifier.LEFT, - "Total Xp: ${addCommas(totalXp.toString())}", - posX - 65, - 28 + DrawText( + FontType.SMALL, + fromColor(Color.WHITE), + TextModifier.LEFT, + "Total Xp: ${addCommas(totalXp.toString())}", + posX - 65, + 28 ) } + private fun drawRuneliteXPBox() { + val boxHeight = 29 + val boxWidth = 119 + val posX = API.GetWindowDimensions().width + + val innerBorderColor = Color(90, 82, 69).rgb + val outerBorderColor = Color(56,48,35).rgb + + val offset = if(API.GetWindowMode() == WindowMode.FIXED) 251 else 225 + val boxStart = posX - (offset + boxWidth) + val yOffset = if(API.GetWindowMode() == WindowMode.FIXED) 4 else 0 + + val lvlIcon = 898; + val sprite = spriteCache.getOrPut(lvlIcon){ + val imageStream: InputStream = plugin::class.java.getResourceAsStream("res/rl-lvls.png") + imageStream.use { imageStream -> + val image: BufferedImage = ImageIO.read(imageStream) + API.GetSpriteFromPNG(image) + } + } + + // Draw a simple rectangle instead of the default box design + API.FillRect(boxStart, yOffset, boxWidth, boxHeight, innerBorderColor, 150) + drawTextWithDropShadow(boxStart+boxWidth-4, 18+yOffset, Color.WHITE, addCommas(totalXp.toString())) + + // Inner Border + API.DrawRect(boxStart+1, 1+yOffset, boxWidth-2, boxHeight-2, innerBorderColor) + // redraw around the border + API.DrawRect(boxStart, yOffset, boxWidth, boxHeight, outerBorderColor) + sprite?.render(boxStart + 3, 3+yOffset) + } + data class XPGain(val skill: Int, val xp: Int, var currentPos: Int) - fun addCommas(num: String): String{ + fun addCommas(num: String): String { var newString = "" - if(num.length > 9){ + if (num.length > 9) { return "Lots!" } var counter = 1 num.reversed().forEach { - if(counter % 3 == 0 && counter != num.length){ + if (counter % 3 == 0 && counter != num.length) { newString += "$it," } else { newString += it @@ -140,4 +251,10 @@ class plugin : Plugin() { } return newString.reversed() } + + private fun drawTextWithDropShadow(x: Int, y: Int, color: Color, text: String, mod : TextModifier = TextModifier.RIGHT) { + DrawText(FontType.SMALL, fromColor(Color(0)), mod, text, x + 1, y + 1) + DrawText(FontType.SMALL, fromColor(color), mod, text, x, y) + } } + diff --git a/plugin-playground/src/main/kotlin/XPDropPlugin/plugin.properties b/plugin-playground/src/main/kotlin/XPDropPlugin/plugin.properties index 35027ea..53b820f 100644 --- a/plugin-playground/src/main/kotlin/XPDropPlugin/plugin.properties +++ b/plugin-playground/src/main/kotlin/XPDropPlugin/plugin.properties @@ -1,3 +1,3 @@ AUTHOR='Ceikry' DESCRIPTION='Draws nice and clean experience drops onto the screen.' -VERSION=1.2 +VERSION=1.3 diff --git a/plugin-playground/src/main/kotlin/XPDropPlugin/res/rl-lvls.png b/plugin-playground/src/main/kotlin/XPDropPlugin/res/rl-lvls.png new file mode 100644 index 0000000..c840a49 Binary files /dev/null and b/plugin-playground/src/main/kotlin/XPDropPlugin/res/rl-lvls.png differ diff --git a/plugin-playground/src/main/kotlin/XPDropPlugin/res/xpIco.png b/plugin-playground/src/main/kotlin/XPDropPlugin/res/xpIco.png new file mode 100644 index 0000000..c68a872 Binary files /dev/null and b/plugin-playground/src/main/kotlin/XPDropPlugin/res/xpIco.png differ