Introduce XPGlobesPlugin

This commit is contained in:
Pyrethus 2024-01-25 04:27:43 +02:00
parent 7957cdc8a6
commit be1aac7fe7
4 changed files with 288 additions and 0 deletions

View file

@ -0,0 +1,22 @@
package XPGlobesPlugin
import java.awt.Color
object Constants {
const val SKILL_COUNT = 24
const val MAX_LEVEL = 99
const val INVALID_LEVEL = -1
const val INVALID_XP = -1
const val GLOBE_LIFETIME = 5000L // 5 seconds
const val GLOBES_Y_OFFSET = 48 // y-offset in screen space where globes are drawn
val GLOBE_BKG_COLOR: Color = Color.GRAY
const val GLOBE_BKG_SIZE = 33 // Size of the globe background
val GLOBE_BORDER_COLOR: Color = Color.BLACK
const val GLOBE_BORDER_WIDTH = 1 // Width of the globe border
val GLOBE_XP_ARC_COLOR: Color = Color.YELLOW
val GLOBE_XP_ARC_LEVEL_UP_COLOR: Color = Color.BLUE
const val GLOBE_XP_ARC_WIDTH = 3 // Width of the Xp arc
const val MAX_GLOBES = 6 // maximum number of globes we will draw on resizable clients
const val MAX_GLOBES_SD = 3 // maximum number of globes we will draw on fixed clients
const val GLOBE_PADDING = 3 // horizontal padding between globes
}

View file

@ -0,0 +1,40 @@
package XPGlobesPlugin
import plugin.api.API
import rt4.Sprite
object XPSprites {
fun getSpriteForSkill(skillId: Int) : Sprite? {
return API.GetSprite(getSpriteId(skillId))
}
private fun getSpriteId(skillId: Int) : Int {
return when (skillId) {
0 -> 197
1 -> 199
2 -> 198
3 -> 203
4 -> 200
5 -> 201
6 -> 202
7 -> 212
8 -> 214
9 -> 208
10 -> 211
11 -> 213
12 -> 207
13 -> 210
14 -> 209
15 -> 205
16 -> 204
17 -> 206
18 -> 216
19 -> 217
20 -> 215
21 -> 220
22 -> 221
23 -> 222
else -> 222
}
}
}

View file

@ -0,0 +1,45 @@
package XPGlobesPlugin
object XPTable {
// source for the experience table: https://oldschool.runescape.wiki/w/Experience#Experience_table
private val xpTable = arrayOf(
0, 83, 174, 276, 388, 512, 650, 801, 969, 1154,
1358, 1584, 1833, 2107, 2411, 2746, 3115, 3523, 3973, 4470,
5018, 5624, 6291, 7028, 7842, 8740, 9730, 10824, 12031, 13363,
14833, 16456, 18247, 20224, 22406, 24815, 27473, 30408, 33648, 37224,
41171, 45529, 50339, 55649, 61512, 67983, 75127, 83014, 91721, 101333,
111945, 123660, 136594, 150872, 166636, 184040, 203254, 224466, 247886, 273742,
302288, 333804, 368599, 407015, 449428, 496254, 547953, 605032, 668051, 737627,
814445, 899257, 992895, 1096278, 1210421, 1336443, 1475581, 1629200, 1798808, 1986068,
2192818, 2421087, 2673114, 2951373, 3258594, 3597792, 3972294, 4385776, 4842295, 5346332,
5902831, 6517253, 7195629, 7944614, 8771558, 9684577, 10692629, 11805606, 13034431
)
fun getXpRequiredForLevel(level: Int): Int {
if (level in 1..xpTable.size) {
return xpTable[level - 1]
}
return 0
}
fun getLevelForXp(xp: Int): Pair<Int, Int> {
var lowIndex = 0
var highIndex = xpTable.size - 1
while (lowIndex <= highIndex) {
val midIndex = (lowIndex + highIndex) / 2
when {
xp < xpTable[midIndex] -> highIndex = midIndex - 1
xp >= xpTable[midIndex + 1] -> lowIndex = midIndex + 1
else -> {
val currentLevel = midIndex + 1
val xpGained = xp - xpTable[midIndex]
return Pair(currentLevel, xpGained)
}
}
}
return Pair(Constants.INVALID_LEVEL, 0) // If xp is above all defined levels
}
}

View file

@ -0,0 +1,181 @@
package XPGlobesPlugin
import rt4.Sprite
import plugin.Plugin
import plugin.annotations.PluginMeta
import plugin.api.*
import java.awt.Color
import java.awt.geom.Arc2D
import java.awt.image.BufferedImage
@PluginMeta(
author = "Pyrethus",
description = "Draws experience globes (level progress) on experience gains.",
version = 0.9
)
class plugin : Plugin() {
private var xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, Constants.INVALID_XP, 0L, null) }
private var lastGain = 0L
private var backgroundSprite: Sprite? = null
private var borderSprite: Sprite? = null
override fun Draw(deltaTime: Long) {
if (System.currentTimeMillis() - lastGain >= Constants.GLOBE_LIFETIME)
return
var posX = API.GetWindowDimensions().width / 2
val posY = API.GetWindowDimensions().height / 4
if (API.GetWindowMode() == WindowMode.FIXED) {
posX += 60
}
API.ClipRect(0, 0, posX * 2, posY * 4)
// update globes
val activeGlobes = ArrayList<XPGlobe>()
for (xpGlobe in xpGlobes) {
val globeDelta = System.currentTimeMillis() - xpGlobe.timestamp
if (globeDelta >= Constants.GLOBE_LIFETIME) {
xpGlobe.timestamp = 0L // dead
}
if (xpGlobe.timestamp != 0L) {
activeGlobes.add(xpGlobe) // alive
}
}
val maxGlobes = if (API.GetWindowMode() == WindowMode.FIXED) Constants.MAX_GLOBES_SD else Constants.MAX_GLOBES
val globeCount = if (activeGlobes.size > maxGlobes) maxGlobes else activeGlobes.size
val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions()
val globeSize = backgroundSize + globeBorder * 2 + xpBorder * 2
val allGlobesWidth = globeCount * (globeSize + Constants.GLOBE_PADDING) - Constants.GLOBE_PADDING
var globePosX = posX - (allGlobesWidth / 2)
// render globes
activeGlobes.take(globeCount).forEach { xpGlobe ->
drawXpGlobe(xpGlobe, globePosX)
globePosX += globeSize + Constants.GLOBE_PADDING // Update globePosX for the next globe
}
}
override fun OnXPUpdate(skillId: Int, xp: Int) {
if (xpGlobes[skillId].xp == Constants.INVALID_XP) {
xpGlobes[skillId].xp = xp
xpGlobes[skillId].prevXp = xp
return
}
if (xp == xpGlobes[skillId].xp) {
return
}
val prevXp = xpGlobes[skillId].xp
xpGlobes[skillId].xp = xp
xpGlobes[skillId].prevXp = prevXp
xpGlobes[skillId].timestamp = 0
val (prevLevel, _) = XPTable.getLevelForXp(prevXp)
val (level, gainedXp) = XPTable.getLevelForXp(xp)
// we do not draw XP globes for level >= MAX_LEVEL
if (level != Constants.INVALID_LEVEL && level < Constants.MAX_LEVEL) {
var arcWeight = 1.0
var arcColor = Constants.GLOBE_XP_ARC_LEVEL_UP_COLOR
if (level == prevLevel) {
val levelXpDiff = XPTable.getXpRequiredForLevel(level + 1) - XPTable.getXpRequiredForLevel(level)
arcWeight = gainedXp.toDouble() / levelXpDiff.toDouble()
arcColor = Constants.GLOBE_XP_ARC_COLOR
}
// remember timestamps
lastGain = System.currentTimeMillis()
xpGlobes[skillId].timestamp = lastGain
val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions()
val globeSize = backgroundSize + globeBorder * 2 + xpBorder * 2
xpGlobes[skillId].arcSprite = createArcSprite(backgroundSize + xpBorder * 2, arcColor, arcWeight)
if (borderSprite == null) {
borderSprite = createArcSprite(globeSize, Constants.GLOBE_BORDER_COLOR, 1.0)
}
if (backgroundSprite == null) {
backgroundSprite = createArcSprite(backgroundSize, Constants.GLOBE_BKG_COLOR, 1.0)
}
}
}
override fun OnLogout() {
lastGain = 0L
xpGlobes = Array(24) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, Constants.INVALID_XP, 0, null) }
}
data class XPGlobe(val skillId: Int, var prevXp: Int, var xp: Int, var timestamp: Long, var arcSprite: Sprite?)
private fun getGlobeDimensions() : Triple<Int, Int, Int> {
val backgroundSize = Constants.GLOBE_BKG_SIZE
val globeBorder = Constants.GLOBE_BORDER_WIDTH
val xpBorder: Int = Constants.GLOBE_XP_ARC_WIDTH
return Triple(backgroundSize, globeBorder, xpBorder)
}
private fun drawXpGlobe(globe: XPGlobe, posX: Int, posY: Int = Constants.GLOBES_Y_OFFSET) {
val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions()
val totalBorder = globeBorder + xpBorder
// rendering background
borderSprite?.render(posX, posY)
globe.arcSprite?.render(posX + globeBorder, posY + globeBorder)
backgroundSprite?.render(posX + totalBorder, posY + totalBorder)
// rendering skill sprite
val skillSprite = XPSprites.getSpriteForSkill(globe.skillId)
val spriteWidth = skillSprite?.anInt1860 ?: 0 // sprite width without trimmed pixels
val spriteHeight = skillSprite?.anInt1866 ?: 0 // sprite height without trimmed pixels
val xOffset = (backgroundSize - spriteWidth) / 2
val yOffset = (backgroundSize - spriteHeight) / 2
val drawX = posX + totalBorder + xOffset
val drawY = posY + totalBorder + yOffset
skillSprite?.render(drawX, drawY)
}
private fun createArcSprite(size: Int, arcColor: Color, weight: Double): Sprite {
return SpritePNGLoader.getImageIndexedSprite(createArcImage(size, arcColor, weight))
}
private fun createArcImage(size: Int, arcColor: Color, weight: Double): BufferedImage {
val image = BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB)
val graphics = image.createGraphics()
// Enable antialiasing for smoother circle edges
graphics.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON)
// Set the color for the circle
graphics.color = arcColor
// Calculate the angle and starting angle based on the weight
val angle = weight * 360
val startAngle = 360 * (0.75 - weight)
// Draw the portion of the circle
graphics.fill(Arc2D.Double(0.0, 0.0, size.toDouble(), size.toDouble(), startAngle, angle, Arc2D.PIE))
// Dispose graphics to release resources
graphics.dispose()
return image
}
}