From be1aac7fe7bd66163462eb1b188db0ea11b16051 Mon Sep 17 00:00:00 2001 From: Pyrethus Date: Thu, 25 Jan 2024 04:27:43 +0200 Subject: [PATCH 1/6] Introduce XPGlobesPlugin --- .../main/kotlin/XPGlobesPlugin/Constants.kt | 22 +++ .../main/kotlin/XPGlobesPlugin/XPSprites.kt | 40 ++++ .../src/main/kotlin/XPGlobesPlugin/XPTable.kt | 45 +++++ .../src/main/kotlin/XPGlobesPlugin/plugin.kt | 181 ++++++++++++++++++ 4 files changed, 288 insertions(+) create mode 100644 plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt create mode 100644 plugin-playground/src/main/kotlin/XPGlobesPlugin/XPSprites.kt create mode 100644 plugin-playground/src/main/kotlin/XPGlobesPlugin/XPTable.kt create mode 100644 plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt new file mode 100644 index 0000000..6e53496 --- /dev/null +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt @@ -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 +} diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/XPSprites.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/XPSprites.kt new file mode 100644 index 0000000..cc3e05a --- /dev/null +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/XPSprites.kt @@ -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 + } + } +} diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/XPTable.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/XPTable.kt new file mode 100644 index 0000000..ff6ddd4 --- /dev/null +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/XPTable.kt @@ -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 { + 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 + } +} diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt new file mode 100644 index 0000000..85afdd3 --- /dev/null +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt @@ -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() + 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 { + 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 + } +} From d730ff65c433de19f4160fc3273022207587be21 Mon Sep 17 00:00:00 2001 From: Pyrethus Date: Thu, 25 Jan 2024 07:38:23 +0200 Subject: [PATCH 2/6] Avoid magic numbers --- plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt index 85afdd3..8c99b7e 100644 --- a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt @@ -16,6 +16,7 @@ import java.awt.image.BufferedImage 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 @@ -62,6 +63,7 @@ class plugin : Plugin() { } } + override fun OnXPUpdate(skillId: Int, xp: Int) { if (xpGlobes[skillId].xp == Constants.INVALID_XP) { xpGlobes[skillId].xp = xp @@ -76,7 +78,7 @@ class plugin : Plugin() { val prevXp = xpGlobes[skillId].xp xpGlobes[skillId].xp = xp xpGlobes[skillId].prevXp = prevXp - xpGlobes[skillId].timestamp = 0 + xpGlobes[skillId].timestamp = 0L val (prevLevel, _) = XPTable.getLevelForXp(prevXp) val (level, gainedXp) = XPTable.getLevelForXp(xp) @@ -112,7 +114,7 @@ class plugin : Plugin() { override fun OnLogout() { lastGain = 0L - xpGlobes = Array(24) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, Constants.INVALID_XP, 0, null) } + xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, Constants.INVALID_XP, 0L, null) } } @@ -156,6 +158,7 @@ class plugin : Plugin() { 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() From e8bed5216315ec3d1442bf8c47ecedf70aa7294c Mon Sep 17 00:00:00 2001 From: Pyrethus Date: Fri, 26 Jan 2024 01:58:43 +0200 Subject: [PATCH 3/6] Manually adjust globe skill sprite positioning --- .../main/kotlin/XPGlobesPlugin/Constants.kt | 1 + .../main/kotlin/XPGlobesPlugin/XPSprites.kt | 40 +++++++++++++++++++ .../src/main/kotlin/XPGlobesPlugin/plugin.kt | 16 ++++---- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt index 6e53496..2a05b7c 100644 --- a/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt @@ -2,6 +2,7 @@ package XPGlobesPlugin import java.awt.Color + object Constants { const val SKILL_COUNT = 24 const val MAX_LEVEL = 99 diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/XPSprites.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/XPSprites.kt index cc3e05a..0005f2b 100644 --- a/plugin-playground/src/main/kotlin/XPGlobesPlugin/XPSprites.kt +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/XPSprites.kt @@ -3,11 +3,51 @@ package XPGlobesPlugin import plugin.api.API import rt4.Sprite + object XPSprites { + + private val spriteOffsets: Array> = arrayOf( + Pair(1,-1), // attack + Pair(0,0), // defense + Pair(0,0), // strength + Pair(0,1), // health + Pair(0,0), // ranged + Pair(0,0), // prayer + Pair(0,-1), // magic + Pair(0,-1), // cooking + Pair(0,0), // woodcutting + Pair(1,0), // fletching + Pair(3,0), // fishing + Pair(0,-1), // fire-making + Pair(1,0), // crafting + Pair(1,0), // smithing + Pair(2,-1), // mining + Pair(0,1), // herblore + Pair(2,0), // agility + Pair(0,0), // thieving + Pair(-1,0), // slayer + Pair(0,0), // farming + Pair(0,0), // runecrafting + Pair(0,0), // hunter + Pair(1,-1), // construction + Pair(2,-1), // summoning + ) + + fun getSpriteForSkill(skillId: Int) : Sprite? { return API.GetSprite(getSpriteId(skillId)) } + + fun getSpriteOffsetForSkill(skillId: Int) : Pair { + if (skillId < 0 || skillId >= Constants.SKILL_COUNT) { + return Pair(0,0) + } + + return spriteOffsets[skillId]; + } + + private fun getSpriteId(skillId: Int) : Int { return when (skillId) { 0 -> 197 diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt index 8c99b7e..144f2a0 100644 --- a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt @@ -18,7 +18,7 @@ import java.awt.image.BufferedImage class plugin : Plugin() { - private var xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, Constants.INVALID_XP, 0L, null) } + private var xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null) } private var lastGain = 0L private var backgroundSprite: Sprite? = null private var borderSprite: Sprite? = null @@ -49,6 +49,7 @@ class plugin : Plugin() { 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() @@ -67,7 +68,6 @@ class plugin : Plugin() { override fun OnXPUpdate(skillId: Int, xp: Int) { if (xpGlobes[skillId].xp == Constants.INVALID_XP) { xpGlobes[skillId].xp = xp - xpGlobes[skillId].prevXp = xp return } @@ -77,14 +77,12 @@ class plugin : Plugin() { val prevXp = xpGlobes[skillId].xp xpGlobes[skillId].xp = xp - xpGlobes[skillId].prevXp = prevXp xpGlobes[skillId].timestamp = 0L 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 @@ -114,11 +112,11 @@ class plugin : Plugin() { override fun OnLogout() { lastGain = 0L - xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, Constants.INVALID_XP, 0L, null) } + xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null) } } - data class XPGlobe(val skillId: Int, var prevXp: Int, var xp: Int, var timestamp: Long, var arcSprite: Sprite?) + data class XPGlobe(val skillId: Int, var xp: Int, var timestamp: Long, var arcSprite: Sprite?) private fun getGlobeDimensions() : Triple { @@ -150,7 +148,11 @@ class plugin : Plugin() { val drawX = posX + totalBorder + xOffset val drawY = posY + totalBorder + yOffset - skillSprite?.render(drawX, drawY) + // even if the centering logic is correct, the sprite seems not to be well-centered inside the + // graphic resource. Manually adjust... + val (spriteXOffset, spriteYOffset) = XPSprites.getSpriteOffsetForSkill(globe.skillId) + + skillSprite?.render(drawX + spriteXOffset, drawY + spriteYOffset) } From e0cb4568a142b5d0454ae9829ee33c2bff94afdd Mon Sep 17 00:00:00 2001 From: Pyrethus Date: Fri, 26 Jan 2024 13:06:42 +0200 Subject: [PATCH 4/6] animate globe on level-up --- .../main/kotlin/XPGlobesPlugin/Constants.kt | 9 +- .../src/main/kotlin/XPGlobesPlugin/plugin.kt | 142 +++++++++++++++--- 2 files changed, 133 insertions(+), 18 deletions(-) diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt index 2a05b7c..6990c89 100644 --- a/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/Constants.kt @@ -8,7 +8,7 @@ object Constants { const val MAX_LEVEL = 99 const val INVALID_LEVEL = -1 const val INVALID_XP = -1 - const val GLOBE_LIFETIME = 5000L // 5 seconds + const val GLOBE_LIFETIME = 7000L // 7 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 @@ -20,4 +20,11 @@ object Constants { 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 + const val GLOBE_TEXT_SIZE = 20 // font size of level text + const val GLOBE_TEXT_PULSES = 7 // 7 text pulses per globe lifetime, on level-up + const val GLOBE_TEXT_PULSE_LOW = 0.2F // alpha during text fade-out + const val GLOBE_TEXT_PULSE_HIGH = 0.8F // alpha during text fade-in + val GLOBE_TEXT_FG_COLOR: Color = Color.BLUE // foreground color of level text during level-up + val GLOBE_TEXT_BG_COLOR: Color = Color.BLACK // background color (outline) of text number during level-up + const val GLOBE_TEXT_OUTLINE_WIDTH = 2 // outline width } diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt index 144f2a0..5ef5251 100644 --- a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt @@ -8,6 +8,13 @@ import plugin.api.* import java.awt.Color import java.awt.geom.Arc2D import java.awt.image.BufferedImage +import java.awt.Font +import java.awt.font.FontRenderContext +import java.awt.geom.AffineTransform +import java.awt.Graphics2D +import java.awt.BasicStroke +import kotlin.math.cos +import kotlin.math.PI @PluginMeta( @@ -18,14 +25,14 @@ import java.awt.image.BufferedImage class plugin : Plugin() { - private var xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null) } - private var lastGain = 0L + private var xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null, null ) } + private var hasActiveGlobes = false private var backgroundSprite: Sprite? = null private var borderSprite: Sprite? = null override fun Draw(deltaTime: Long) { - if (System.currentTimeMillis() - lastGain >= Constants.GLOBE_LIFETIME) + if (!hasActiveGlobes) return var posX = API.GetWindowDimensions().width / 2 @@ -43,6 +50,7 @@ class plugin : Plugin() { val globeDelta = System.currentTimeMillis() - xpGlobe.timestamp if (globeDelta >= Constants.GLOBE_LIFETIME) { xpGlobe.timestamp = 0L // dead + xpGlobe.textSprite = null } if (xpGlobe.timestamp != 0L) { @@ -50,6 +58,11 @@ class plugin : Plugin() { } } + if (activeGlobes.isEmpty()) { + hasActiveGlobes = false + return + } + 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() @@ -66,18 +79,19 @@ class plugin : Plugin() { override fun OnXPUpdate(skillId: Int, xp: Int) { - if (xpGlobes[skillId].xp == Constants.INVALID_XP) { - xpGlobes[skillId].xp = xp + val xpGlobe = xpGlobes[skillId] + if (xpGlobe.xp == Constants.INVALID_XP) { + xpGlobe.xp = xp return } - if (xp == xpGlobes[skillId].xp) { + if (xp == xpGlobe.xp) { return } - val prevXp = xpGlobes[skillId].xp - xpGlobes[skillId].xp = xp - xpGlobes[skillId].timestamp = 0L + val prevXp = xpGlobe.xp + xpGlobe.xp = xp + xpGlobe.timestamp = 0L val (prevLevel, _) = XPTable.getLevelForXp(prevXp) val (level, gainedXp) = XPTable.getLevelForXp(xp) @@ -85,20 +99,24 @@ class plugin : Plugin() { if (level != Constants.INVALID_LEVEL && level < Constants.MAX_LEVEL) { var arcWeight = 1.0 var arcColor = Constants.GLOBE_XP_ARC_LEVEL_UP_COLOR + var hasLeveledUp = level != prevLevel - if (level == prevLevel) { + if (!hasLeveledUp) { 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 + // remember timestamp + xpGlobe.timestamp = System.currentTimeMillis() + hasActiveGlobes = true val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions() val globeSize = backgroundSize + globeBorder * 2 + xpBorder * 2 - xpGlobes[skillId].arcSprite = createArcSprite(backgroundSize + xpBorder * 2, arcColor, arcWeight) + if (xpGlobe.textSprite == null) { + // update the arcSprite only if the textSprite has finished the animation + xpGlobe.arcSprite = createArcSprite(backgroundSize + xpBorder * 2, arcColor, arcWeight) + } if (borderSprite == null) { borderSprite = createArcSprite(globeSize, Constants.GLOBE_BORDER_COLOR, 1.0) @@ -106,17 +124,25 @@ class plugin : Plugin() { if (backgroundSprite == null) { backgroundSprite = createArcSprite(backgroundSize, Constants.GLOBE_BKG_COLOR, 1.0) } + + if (hasLeveledUp) { + val fontSize = Constants.GLOBE_TEXT_SIZE + val fgColor = Constants.GLOBE_TEXT_FG_COLOR + val bgColor = Constants.GLOBE_TEXT_BG_COLOR + val outlineSize = Constants.GLOBE_TEXT_OUTLINE_WIDTH + xpGlobes[skillId].textSprite = createTextSprite(fontSize, fgColor, bgColor, outlineSize, level.toString()) + } } } override fun OnLogout() { - lastGain = 0L - xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null) } + hasActiveGlobes = false + xpGlobes = Array(Constants.SKILL_COUNT) { skillId -> XPGlobe(skillId, Constants.INVALID_XP, 0L, null, null) } } - data class XPGlobe(val skillId: Int, var xp: Int, var timestamp: Long, var arcSprite: Sprite?) + data class XPGlobe(val skillId: Int, var xp: Int, var timestamp: Long, var arcSprite: Sprite?, var textSprite: Sprite?) private fun getGlobeDimensions() : Triple { @@ -153,6 +179,88 @@ class plugin : Plugin() { val (spriteXOffset, spriteYOffset) = XPSprites.getSpriteOffsetForSkill(globe.skillId) skillSprite?.render(drawX + spriteXOffset, drawY + spriteYOffset) + + // rendering level-up text animation + if (globe.textSprite != null) { + val clamp: (Float, Float, Float) -> Float = { value, min, max -> + when { + value < min -> min + value > max -> max + else -> value + } + } + + val lerp: (Float, Float, Float) -> Float = { valA, valB, factor -> + valA * (1.0F - factor) + (valB * factor) + } + + val currentTime = System.currentTimeMillis() + var animWeight = (currentTime - globe.timestamp).toFloat() / Constants.GLOBE_LIFETIME + animWeight = clamp(animWeight, 0.0F, 1.0F) + // sample cosine for the text pulse animation effect (cosine offset is to respect number of pulses) + var pulseWeight = cos(animWeight * (PI + PI * 2 * Constants.GLOBE_TEXT_PULSES).toFloat()) + pulseWeight = (pulseWeight + 1.0F) / 2.0F // map pulseWeight from [-1,1] to [0,1] + val pulseIntensity = lerp(Constants.GLOBE_TEXT_PULSE_LOW, Constants.GLOBE_TEXT_PULSE_HIGH, pulseWeight) + + val globeSize = backgroundSize + globeBorder * 2 + xpBorder * 2 + + val textWidth = globe.textSprite?.anInt1860 ?: 0 + val textHeight = globe.textSprite?.anInt1866 ?: 0 + val textXOffset = (globeSize - textWidth) / 2 + val textYOffset = (globeSize - textHeight) / 2 + + val textX = posX + textXOffset + (Constants.GLOBE_TEXT_OUTLINE_WIDTH / 2) + val textY = posY + textYOffset + (Constants.GLOBE_TEXT_OUTLINE_WIDTH / 2) + + val alpha = lerp(0f, 255f, pulseIntensity).toInt() + globe.textSprite?.renderAlpha(textX, textY, alpha) + } + } + + private fun createTextSprite(size: Int, fgColor: Color, bgColor: Color, outlineSize: Int, text: String) : Sprite { + return SpritePNGLoader.getImageIndexedSprite(createTextImage(size, fgColor, bgColor, outlineSize, text)) + } + + private fun createTextImage(fontSize: Int, fgColor: Color, bgColor: Color, outlineSize: Int, text: String) : BufferedImage { + // Create a dummy font to calculate the size of the BufferedImage + val font = Font("Arial", Font.PLAIN, fontSize) + val affineTransform = AffineTransform() + val frc = FontRenderContext(affineTransform, true, true) + val textWidth = font.getStringBounds(text, frc).width.toInt() + outlineSize + val textHeight = font.getStringBounds(text, frc).height.toInt() + outlineSize + + // Create an image that can contain the text + val image = BufferedImage(textWidth, textHeight, BufferedImage.TYPE_INT_ARGB) + val graphics = image.createGraphics() as Graphics2D + + // Enable antialiasing for smoother text + graphics.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON) + + // Set the font + graphics.font = font + + // Calculate position for the text + val metrics = graphics.getFontMetrics(font) + val x = 0 + val y = metrics.ascent + + // Create a glyph vector for the outline + val glyphVector = font.createGlyphVector(frc, text) + val shape = glyphVector.getOutline(x.toFloat(), y.toFloat()) + + // Draw the outline (background color) + graphics.color = bgColor + graphics.stroke = BasicStroke(outlineSize.toFloat()) // Set the outline width + graphics.draw(shape) + + // Fill the text (foreground color) + graphics.color = fgColor + graphics.fill(shape) + + // Dispose graphics to release resources + graphics.dispose() + + return image } From 8257066e9e8ce0836950607f85ea7ad5fcaf8932 Mon Sep 17 00:00:00 2001 From: Pyrethus Date: Sat, 27 Jan 2024 04:04:56 +0200 Subject: [PATCH 5/6] Do not refresh globe timestamp if animation is playing --- .../src/main/kotlin/XPGlobesPlugin/plugin.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt index 5ef5251..16a3737 100644 --- a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt @@ -91,7 +91,6 @@ class plugin : Plugin() { val prevXp = xpGlobe.xp xpGlobe.xp = xp - xpGlobe.timestamp = 0L val (prevLevel, _) = XPTable.getLevelForXp(prevXp) val (level, gainedXp) = XPTable.getLevelForXp(xp) @@ -99,7 +98,7 @@ class plugin : Plugin() { if (level != Constants.INVALID_LEVEL && level < Constants.MAX_LEVEL) { var arcWeight = 1.0 var arcColor = Constants.GLOBE_XP_ARC_LEVEL_UP_COLOR - var hasLeveledUp = level != prevLevel + val hasLeveledUp = level != prevLevel if (!hasLeveledUp) { val levelXpDiff = XPTable.getXpRequiredForLevel(level + 1) - XPTable.getXpRequiredForLevel(level) @@ -107,20 +106,18 @@ class plugin : Plugin() { arcColor = Constants.GLOBE_XP_ARC_COLOR } - // remember timestamp - xpGlobe.timestamp = System.currentTimeMillis() - hasActiveGlobes = true - val (backgroundSize, globeBorder, xpBorder) = getGlobeDimensions() val globeSize = backgroundSize + globeBorder * 2 + xpBorder * 2 if (xpGlobe.textSprite == null) { - // update the arcSprite only if the textSprite has finished the animation + // update arcSprite and timestamp only if the textSprite has finished the animation xpGlobe.arcSprite = createArcSprite(backgroundSize + xpBorder * 2, arcColor, arcWeight) + xpGlobe.timestamp = System.currentTimeMillis() } if (borderSprite == null) { borderSprite = createArcSprite(globeSize, Constants.GLOBE_BORDER_COLOR, 1.0) } + if (backgroundSprite == null) { backgroundSprite = createArcSprite(backgroundSize, Constants.GLOBE_BKG_COLOR, 1.0) } @@ -131,7 +128,10 @@ class plugin : Plugin() { val bgColor = Constants.GLOBE_TEXT_BG_COLOR val outlineSize = Constants.GLOBE_TEXT_OUTLINE_WIDTH xpGlobes[skillId].textSprite = createTextSprite(fontSize, fgColor, bgColor, outlineSize, level.toString()) + xpGlobe.timestamp = System.currentTimeMillis() } + + hasActiveGlobes = true } } From 4a4f6e0d712fd5644d16d6de99b0566d2949dc7e Mon Sep 17 00:00:00 2001 From: Pyrethus Date: Sat, 27 Jan 2024 05:47:11 +0200 Subject: [PATCH 6/6] Fix animation cosine phase-shifted sampling --- plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt index 16a3737..8c651b4 100644 --- a/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt +++ b/plugin-playground/src/main/kotlin/XPGlobesPlugin/plugin.kt @@ -197,8 +197,8 @@ class plugin : Plugin() { val currentTime = System.currentTimeMillis() var animWeight = (currentTime - globe.timestamp).toFloat() / Constants.GLOBE_LIFETIME animWeight = clamp(animWeight, 0.0F, 1.0F) - // sample cosine for the text pulse animation effect (cosine offset is to respect number of pulses) - var pulseWeight = cos(animWeight * (PI + PI * 2 * Constants.GLOBE_TEXT_PULSES).toFloat()) + // sample cosine for the text pulse animation effect (cosine phase-shift is to respect number of pulses) + var pulseWeight = cos(PI + animWeight * (PI * 2 * Constants.GLOBE_TEXT_PULSES).toFloat()).toFloat() pulseWeight = (pulseWeight + 1.0F) / 2.0F // map pulseWeight from [-1,1] to [0,1] val pulseIntensity = lerp(Constants.GLOBE_TEXT_PULSE_LOW, Constants.GLOBE_TEXT_PULSE_HIGH, pulseWeight)